Completed
Push — develop ( a87380...8e8605 )
by Nate
02:36
created

Fields::normalizeQueryInputValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 8
cp 0
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 4
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 MetaField $field
196
     * @param array $values
197
     * @param ElementInterface $element
198
     * @return array
199
     */
200
    protected function getExistingValues(MetaField $field, array $values, ElementInterface $element): array
201
    {
202
        /** @var Element $element */
203
        if (!empty($element->id)) {
204
            $ids = [];
205
206
            foreach ($values as $metaId => $meta) {
207
                if (is_numeric($metaId) && $metaId !== 0) {
208
                    $ids[] = $metaId;
209
                }
210
            }
211
212
            if (!empty($ids)) {
213
                $oldMetaQuery = MetaPlugin::getInstance()->getElements()->getQuery();
214
                $oldMetaQuery->fieldId($field->id);
215
                $oldMetaQuery->ownerId($element->id);
216
                $oldMetaQuery->id($ids);
217
                $oldMetaQuery->limit(null);
218
                $oldMetaQuery->status(null);
219
                $oldMetaQuery->enabledForSite(false);
220
                $oldMetaQuery->siteId($element->siteId);
221
                $oldMetaQuery->indexBy('id');
222
                return $oldMetaQuery->all();
223
            }
224
        }
225
226
        return [];
227
    }
228
229
230
    /*******************************************
231
     * ELEMENT EVENTS
232
     *******************************************/
233
234
    /**
235
     * @param MetaField $field
236
     * @param ElementInterface $element
237
     * @return bool
238
     * @throws \Throwable
239
     */
240
    public function beforeElementDelete(MetaField $field, ElementInterface $element): bool
241
    {
242
        // Delete any meta elements that belong to this element(s)
243
        foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) {
244
            $query = MetaElement::find();
245
            $query->status(null);
246
            $query->enabledForSite(false);
247
            $query->fieldId($field->id);
248
            $query->siteId($siteId);
249
            $query->owner($element);
250
251
            /** @var MetaElement $meta */
252
            foreach ($query->all() as $meta) {
253
                Craft::$app->getElements()->deleteElement($meta);
254
            }
255
        }
256
257
        return true;
258
    }
259
260
    /**
261
     * @param Meta $field
262
     * @param ElementInterface $owner
263
     * @throws \Exception
264
     * @throws \Throwable
265
     * @throws \yii\db\Exception
266
     */
267
    public function afterElementSave(MetaField $field, ElementInterface $owner)
268
    {
269
        /** @var Element $owner */
270
271
        /** @var MetaQuery $query */
272
        $query = $owner->getFieldValue($field->handle);
273
274
        // Skip if the query's site ID is different than the element's
275
        // (Indicates that the value as copied from another site for element propagation)
276
        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...
277
            return;
278
        }
279
280
        if (null === ($elements = $query->getCachedResult())) {
281
            $query = clone $query;
282
            $query->status = null;
283
            $query->enabledForSite = false;
284
            $elements = $query->all(); // existing meta
285
        }
286
287
        $transaction = Craft::$app->getDb()->beginTransaction();
288
        try {
289
            // If this is a preexisting element, make sure that the blocks for this field/owner respect the field's translation setting
290
            if ($query->ownerId) {
291
                $this->applyFieldTranslationSetting($query->ownerId, $query->siteId, $field);
292
            }
293
294
            // If the query is set to fetch blocks of a different owner, we're probably duplicating an element
295
            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...
296
                $this->duplicate($field, $owner, $query, $elements);
297
            } else {
298
                $this->save($field, $owner, $elements);
299
            }
300
301
            $transaction->commit();
302
        } catch (\Exception $e) {
303
            $transaction->rollback();
304
            throw $e;
305
        }
306
307
        return;
308
    }
309
310
    /**
311
     * @param MetaField $field
312
     * @param ElementInterface $owner
313
     * @param MetaQuery $query
314
     * @param array $elements
315
     * @throws \Throwable
316
     * @throws \craft\errors\InvalidElementException
317
     */
318
    private function duplicate(MetaField $field, ElementInterface $owner, MetaQuery $query, array $elements)
319
    {
320
        /** @var Element $owner */
321
322
        $newQuery = clone $query;
323
        $newQuery->ownerId = $owner->id;
324
        if (!$newQuery->exists()) {
325
            // Duplicate for the new owner
326
            $elementsService = Craft::$app->getElements();
327
            foreach ($elements as $element) {
328
                $elementsService->duplicateElement($element, [
329
                    'ownerId' => $owner->id,
330
                    'ownerSiteId' => $field->localize ? $owner->siteId : null
331
                ]);
332
            }
333
        }
334
    }
335
336
    /**
337
     * @param MetaField $field
338
     * @param ElementInterface $owner
339
     * @param MetaElement[] $elements
340
     * @throws Exception
341
     * @throws \Throwable
342
     * @throws \craft\errors\ElementNotFoundException
343
     */
344
    private function save(MetaField $field, ElementInterface $owner, array $elements)
345
    {
346
        /** @var Element $owner */
347
348
        $elementIds = [];
349
350
        // Only propagate the blocks if the owner isn't being propagated
351
        $propagate = !$owner->propagating;
352
353
        /** @var MetaElement $element */
354
        foreach ($elements as $element) {
355
            $element->setOwnerId($owner->id);
356
            $element->ownerSiteId = ($field->localize ? $owner->siteId : null);
357
            $element->propagating = $owner->propagating;
358
359
            Craft::$app->getElements()->saveElement($element, false, $propagate);
360
361
            $elementIds[] = $element->id;
362
        }
363
364
        // Delete any elements that have been removed
365
        $this->deleteOld($field, $owner, $elementIds);
366
    }
367
368
    /**
369
     * @param MetaField $field
370
     * @param ElementInterface $owner
371
     * @param array $excludeIds
372
     * @throws \Throwable
373
     */
374
    private function deleteOld(MetaField $field, ElementInterface $owner, array $excludeIds)
375
    {
376
        /** @var Element $owner */
377
378
        $deleteElementsQuery = MetaElement::find()
379
            ->status(null)
380
            ->enabledForSite(false)
381
            ->ownerId($owner->id)
382
            ->fieldId($field->id)
383
            ->where(['not', ['elements.id' => $excludeIds]]);
384
385
        if ($field->localize) {
386
            $deleteElementsQuery->ownerSiteId($owner->siteId);
387
        } else {
388
            $deleteElementsQuery->siteId($owner->siteId);
389
        }
390
391
        foreach ($deleteElementsQuery->all() as $deleteElement) {
392
            Craft::$app->getElements()->deleteElement($deleteElement);
393
        }
394
    }
395
396
    /**
397
     * Applies the field's translation setting to a set of blocks.
398
     *
399
     * @param int $ownerId
400
     * @param int $ownerSiteId
401
     * @param Meta $field
402
     * @throws Exception
403
     * @throws \Throwable
404
     * @throws \craft\errors\ElementNotFoundException
405
     */
406
    private function applyFieldTranslationSetting(int $ownerId, int $ownerSiteId, MetaField $field)
407
    {
408
        // If the field is translatable, see if there are any global blocks that should be localized
409
        if ($field->localize) {
410
            $this->saveFieldTranslations($field, $ownerId, $ownerSiteId);
411
        } else {
412
413
            // Otherwise, see if the field has any localized blocks that should be deleted
414
            foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) {
415
                if ($siteId != $ownerSiteId) {
416
                    $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...
417
                        ->fieldId($field->id)
418
                        ->ownerId($ownerId)
419
                        ->status(null)
420
                        ->enabledForSite(false)
421
                        ->limit(null)
422
                        ->siteId($siteId)
423
                        ->ownerSiteId($siteId)
424
                        ->all();
425
426
                    foreach ($elements as $element) {
427
                        Craft::$app->getElements()->deleteElement($element);
428
                    }
429
                }
430
            }
431
        }
432
    }
433
434
    /**
435
     * @param MetaField $field
436
     * @param int $ownerId
437
     * @param int $ownerSiteId
438
     * @throws Exception
439
     * @throws \Throwable
440
     * @throws \craft\errors\ElementNotFoundException
441
     */
442
    private function saveFieldTranslations(MetaField $field, int $ownerId, int $ownerSiteId)
443
    {
444
        $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...
445
            ->fieldId($field->id)
446
            ->ownerId($ownerId)
447
            ->status(null)
448
            ->enabledForSite(false)
449
            ->limit(null)
450
            ->siteId($ownerSiteId)
451
            ->ownerSiteId(':empty:');
452
453
        $elements = $elementQuery->all();
454
455
        if (empty($elements)) {
456
            return;
457
        }
458
459
        // Prefetch the blocks in all the other sites, in case they have any localized content
460
        $otherSiteMeta = $this->getOtherSiteMeta($elementQuery, $ownerSiteId);
461
462
        // Explicitly assign the current site's blocks to the current site
463
        foreach ($elements as $element) {
464
            $element->ownerSiteId = $ownerSiteId;
465
            Craft::$app->getElements()->saveElement($element, false);
466
        }
467
468
        // Now save the other sites' blocks as new site-specific blocks
469
        foreach ($otherSiteMeta as $siteId => $siteElements) {
470
            foreach ($siteElements as $element) {
471
                $element->id = null;
472
                $element->contentId = null;
473
                $element->siteId = (int)$siteId;
474
                $element->ownerSiteId = (int)$siteId;
475
                Craft::$app->getElements()->saveElement($element, false);
476
            }
477
        }
478
    }
479
480
    /**
481
     * @param MetaQuery $query
482
     * @param int $ownerSiteId
483
     * @return array
484
     */
485
    private function getOtherSiteMeta(MetaQuery $query, int $ownerSiteId)
486
    {
487
        // Find any relational fields
488
        $relationFields = $this->getRelationFields($query->all());
489
490
        $otherSiteMeta = [];
491
        foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) {
492
            if ($siteId != $ownerSiteId) {
493
                /** @var MetaElement[] $siteElements */
494
                $siteElements = $otherSiteMeta[$siteId] = $query->siteId($siteId)->all();
495
496
                // Hard-set the relation IDs
497
                foreach ($siteElements as $element) {
498
                    foreach ($relationFields as $handle) {
499
                        /** @var ElementQueryInterface $relationQuery */
500
                        $relationQuery = $element->getFieldValue($handle);
501
                        $element->setFieldValue($handle, $relationQuery->ids());
502
                    }
503
                }
504
            }
505
        }
506
507
        return $otherSiteMeta;
508
    }
509
510
    /**
511
     * @param array $elements
512
     * @return array
513
     */
514
    private function getRelationFields(array $elements)
515
    {
516
        // Find any relational fields on these blocks
517
        $relationFields = [];
518
519
        foreach ($elements as $element) {
520
            foreach ($element->getFieldLayout()->getFields() as $field) {
521
                if ($field instanceof BaseRelationField) {
522
                    $relationFields[] = $field->handle;
523
                }
524
            }
525
            break;
526
        }
527
528
        return $relationFields;
529
    }
530
531
    /**
532
     * @inheritdoc
533
     */
534
    protected function normalizeQueryInputValue(
535
        FieldInterface $field,
536
        $value,
537
        int &$sortOrder,
538
        ElementInterface $element = null
539
    ): SortableAssociationInterface {
540
541
        throw new Exception(__METHOD__ . ' is not implemented');
542
    }
543
}
544