Fields::deleteOld()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 0
cts 17
cp 0
rs 9.584
c 0
b 0
f 0
cc 3
nc 4
nop 3
crap 12
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,
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
100
        $value,
101
        ElementInterface $element = null
0 ignored issues
show
Unused Code introduced by
The parameter $element is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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;
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) {
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
290
            // field's translation setting
291
            if ($query->ownerId) {
292
                $this->applyFieldTranslationSetting($query->ownerId, $query->siteId, $field);
0 ignored issues
show
Bug introduced by
It seems like $query->ownerId can also be of type array<integer,integer>; however, flipbox\meta\services\Fi...eldTranslationSetting() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
293
            }
294
295
            // If the query is set to fetch blocks of a different owner, we're probably duplicating an element
296
            if ($query->ownerId && $query->ownerId != $owner->id) {
297
                $this->duplicate($field, $owner, $query, $elements);
298
            } else {
299
                $this->save($field, $owner, $elements);
300
            }
301
302
            $transaction->commit();
303
        } catch (\Exception $e) {
304
            $transaction->rollback();
305
            throw $e;
306
        }
307
308
        return;
309
    }
310
311
    /**
312
     * @param MetaField $field
313
     * @param ElementInterface $owner
314
     * @param MetaQuery $query
315
     * @param array $elements
316
     * @throws \Throwable
317
     * @throws \craft\errors\InvalidElementException
318
     */
319
    private function duplicate(MetaField $field, ElementInterface $owner, MetaQuery $query, array $elements)
320
    {
321
        /** @var Element $owner */
322
323
        $newQuery = clone $query;
324
        $newQuery->ownerId = $owner->id;
325
        if (!$newQuery->exists()) {
326
            // Duplicate for the new owner
327
            $elementsService = Craft::$app->getElements();
328
            foreach ($elements as $element) {
329
                $elementsService->duplicateElement($element, [
330
                    'ownerId' => $owner->id,
331
                    'ownerSiteId' => $field->localize ? $owner->siteId : null
332
                ]);
333
            }
334
        }
335
    }
336
337
    /**
338
     * @param MetaField $field
339
     * @param ElementInterface $owner
340
     * @param MetaElement[] $elements
341
     * @throws Exception
342
     * @throws \Throwable
343
     * @throws \craft\errors\ElementNotFoundException
344
     */
345
    private function save(MetaField $field, ElementInterface $owner, array $elements)
346
    {
347
        /** @var Element $owner */
348
349
        $elementIds = [];
350
351
        // Only propagate the blocks if the owner isn't being propagated
352
        $propagate = !$owner->propagating;
353
354
        /** @var MetaElement $element */
355
        foreach ($elements as $element) {
356
            $element->setOwner($owner);
357
            $element->ownerSiteId = ($field->localize ? $owner->siteId : null);
358
            $element->propagating = $owner->propagating;
359
360
            Craft::$app->getElements()->saveElement($element, false, $propagate);
361
362
            $elementIds[] = $element->id;
363
        }
364
365
        // Delete any elements that have been removed
366
        $this->deleteOld($field, $owner, $elementIds);
367
    }
368
369
    /**
370
     * @param MetaField $field
371
     * @param ElementInterface $owner
372
     * @param array $excludeIds
373
     * @throws \Throwable
374
     */
375
    private function deleteOld(MetaField $field, ElementInterface $owner, array $excludeIds)
376
    {
377
        /** @var Element $owner */
378
379
        $deleteElementsQuery = MetaElement::find()
380
            ->status(null)
381
            ->enabledForSite(false)
382
            ->ownerId($owner->id)
383
            ->fieldId($field->id)
384
            ->where(['not', ['elements.id' => $excludeIds]]);
385
386
        if ($field->localize) {
387
            $deleteElementsQuery->ownerSiteId($owner->siteId);
388
        } else {
389
            $deleteElementsQuery->siteId($owner->siteId);
390
        }
391
392
        foreach ($deleteElementsQuery->all() as $deleteElement) {
393
            Craft::$app->getElements()->deleteElement($deleteElement);
394
        }
395
    }
396
397
    /**
398
     * Applies the field's translation setting to a set of blocks.
399
     *
400
     * @param int $ownerId
401
     * @param int $ownerSiteId
402
     * @param Meta $field
403
     * @throws Exception
404
     * @throws \Throwable
405
     * @throws \craft\errors\ElementNotFoundException
406
     */
407
    private function applyFieldTranslationSetting(int $ownerId, int $ownerSiteId, MetaField $field)
408
    {
409
        // If the field is translatable, see if there are any global blocks that should be localized
410
        if ($field->localize) {
411
            $this->saveFieldTranslations($field, $ownerId, $ownerSiteId);
412
        } else {
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
                    /** @var MetaQuery $elements */
417
                    $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...
418
                        ->fieldId($field->id)
419
                        ->ownerId($ownerId)
420
                        ->status(null)
421
                        ->enabledForSite(false)
422
                        ->limit(null)
423
                        ->siteId($siteId)
424
                        ->ownerSiteId($siteId)
425
                        ->all();
426
427
                    foreach ($elements as $element) {
428
                        Craft::$app->getElements()->deleteElement($element);
429
                    }
430
                }
431
            }
432
        }
433
    }
434
435
    /**
436
     * @param MetaField $field
437
     * @param int $ownerId
438
     * @param int $ownerSiteId
439
     * @throws Exception
440
     * @throws \Throwable
441
     * @throws \craft\errors\ElementNotFoundException
442
     */
443
    private function saveFieldTranslations(MetaField $field, int $ownerId, int $ownerSiteId)
444
    {
445
        /** @var MetaQuery $elements */
446
        $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...
447
            ->fieldId($field->id)
448
            ->ownerId($ownerId)
449
            ->status(null)
450
            ->enabledForSite(false)
451
            ->limit(null)
452
            ->siteId($ownerSiteId)
453
            ->ownerSiteId(':empty:');
454
455
        $elements = $elementQuery->all();
456
457
        if (empty($elements)) {
458
            return;
459
        }
460
461
        // Prefetch the blocks in all the other sites, in case they have any localized content
462
        $otherSiteMeta = $this->getOtherSiteMeta($elementQuery, $ownerSiteId);
463
464
        // Explicitly assign the current site's blocks to the current site
465
        foreach ($elements as $element) {
466
            $element->ownerSiteId = $ownerSiteId;
467
            Craft::$app->getElements()->saveElement($element, false);
468
        }
469
470
        // Now save the other sites' blocks as new site-specific blocks
471
        foreach ($otherSiteMeta as $siteId => $siteElements) {
472
            foreach ($siteElements as $element) {
473
                $element->id = null;
474
                $element->contentId = null;
475
                $element->siteId = (int)$siteId;
476
                $element->ownerSiteId = (int)$siteId;
477
                Craft::$app->getElements()->saveElement($element, false);
478
            }
479
        }
480
    }
481
482
    /**
483
     * @param MetaQuery $query
484
     * @param int $ownerSiteId
485
     * @return array
486
     */
487
    private function getOtherSiteMeta(MetaQuery $query, int $ownerSiteId)
488
    {
489
        // Find any relational fields
490
        $relationFields = $this->getRelationFields($query->all());
491
492
        $otherSiteMeta = [];
493
        foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) {
494
            if ($siteId != $ownerSiteId) {
495
                /** @var MetaElement[] $siteElements */
496
                $siteElements = $otherSiteMeta[$siteId] = $query->siteId($siteId)->all();
497
498
                // Hard-set the relation IDs
499
                foreach ($siteElements as $element) {
500
                    foreach ($relationFields as $handle) {
501
                        /** @var ElementQueryInterface $relationQuery */
502
                        $relationQuery = $element->getFieldValue($handle);
503
                        $element->setFieldValue($handle, $relationQuery->ids());
504
                    }
505
                }
506
            }
507
        }
508
509
        return $otherSiteMeta;
510
    }
511
512
    /**
513
     * @param array $elements
514
     * @return array
515
     */
516
    private function getRelationFields(array $elements)
517
    {
518
        // Find any relational fields on these blocks
519
        $relationFields = [];
520
521
        foreach ($elements as $element) {
522
            foreach ($element->getFieldLayout()->getFields() as $field) {
523
                if ($field instanceof BaseRelationField) {
524
                    $relationFields[] = $field->handle;
525
                }
526
            }
527
            break;
528
        }
529
530
        return $relationFields;
531
    }
532
533
    /**
534
     * @inheritdoc
535
     */
536
    protected function normalizeQueryInputValue(
537
        FieldInterface $field,
538
        $value,
539
        int &$sortOrder,
540
        ElementInterface $element = null
541
    ): SortableAssociationInterface {
542
543
        throw new Exception(__METHOD__ . ' is not implemented');
544
    }
545
}
546