uawdijnntqw1x1x1
IP : 216.73.216.107
Hostname : toronto-dev2
Kernel : Linux toronto-dev2 4.15.0-213-generic #224-Ubuntu SMP Mon Jun 19 13:30:12 UTC 2023 x86_64
Disable Function : None :)
OS : Linux
PATH:
/
srv
/
users
/
craft4
/
apps
/
craft4-newsite-space
/
vendor
/
craftcms
/
.
/
.
/
cms
/
src
/
.
/
services
/
.
/
Matrix.php
/
/
<?php /** * @link https://craftcms.com/ * @copyright Copyright (c) Pixel & Tonic, Inc. * @license https://craftcms.github.io/license/ */ namespace craft\services; use Craft; use craft\base\ElementInterface; use craft\base\Field; use craft\db\Query; use craft\db\Table; use craft\elements\db\MatrixBlockQuery; use craft\elements\MatrixBlock; use craft\errors\MatrixBlockTypeNotFoundException; use craft\events\ConfigEvent; use craft\fields\Matrix as MatrixField; use craft\helpers\ArrayHelper; use craft\helpers\Db; use craft\helpers\ElementHelper; use craft\helpers\Html; use craft\migrations\CreateMatrixContentTable; use craft\models\FieldLayout; use craft\models\MatrixBlockType; use craft\models\Site; use craft\records\MatrixBlockType as MatrixBlockTypeRecord; use craft\validators\StringValidator; use craft\web\assets\matrix\MatrixAsset; use craft\web\View; use Illuminate\Support\Collection; use Throwable; use yii\base\Component; use yii\base\Exception; use yii\base\InvalidArgumentException; /** * The Matrix service provides APIs for managing Matrix fields. * * An instance of the service is available via [[\craft\base\ApplicationTrait::getMatrix()|`Craft::$app->matrix`]]. * * @author Pixel & Tonic, Inc. <support@pixelandtonic.com> * @since 3.0.0 */ class Matrix extends Component { /** * @var MatrixBlockType[]|null[] */ private array $_blockTypesById = []; /** * @var MatrixBlockType[][] */ private array $_blockTypesByFieldId = []; /** * @var bool[] */ private array $_fetchedAllBlockTypesForFieldId = []; /** * @var MatrixBlockTypeRecord[] */ private array $_blockTypeRecordsById = []; /** * @var string[] */ private array $_uniqueBlockTypeAndFieldHandles = []; /** * Returns the block types for a given Matrix field. * * @param int $fieldId The Matrix field ID. * @return MatrixBlockType[] An array of block types. */ public function getBlockTypesByFieldId(int $fieldId): array { if (!empty($this->_fetchedAllBlockTypesForFieldId[$fieldId])) { return $this->_blockTypesByFieldId[$fieldId]; } $this->_blockTypesByFieldId[$fieldId] = []; $results = $this->_createBlockTypeQuery() ->where(['bt.fieldId' => $fieldId]) ->all(); foreach ($results as $result) { $blockType = new MatrixBlockType($result); $this->_blockTypesById[$blockType->id] = $blockType; $this->_blockTypesByFieldId[$fieldId][] = $blockType; } $this->_fetchedAllBlockTypesForFieldId[$fieldId] = true; return $this->_blockTypesByFieldId[$fieldId]; } /** * Returns all the block types. * * @return MatrixBlockType[] An array of block types. * @since 3.3.0 */ public function getAllBlockTypes(): array { $results = $this->_createBlockTypeQuery() ->innerJoin(['f' => Table::FIELDS], '[[f.id]] = [[bt.fieldId]]') ->where(['f.type' => MatrixField::class]) ->all(); foreach ($results as $key => $result) { $results[$key] = new MatrixBlockType($result); } return $results; } /** * Returns a block type by its ID. * * @param int $blockTypeId The block type ID. * @return MatrixBlockType|null The block type, or `null` if it didn’t exist. */ public function getBlockTypeById(int $blockTypeId): ?MatrixBlockType { if (array_key_exists($blockTypeId, $this->_blockTypesById)) { return $this->_blockTypesById[$blockTypeId]; } $result = $this->_createBlockTypeQuery() ->where(['bt.id' => $blockTypeId]) ->one(); return $this->_blockTypesById[$blockTypeId] = $result ? new MatrixBlockType($result) : null; } /** * Validates a block type. * * If the block type doesn’t validate, any validation errors will be stored on the block type. * * @param MatrixBlockType $blockType The block type. * @return bool Whether the block type validated. */ public function validateBlockType(MatrixBlockType $blockType): bool { $validates = true; $blockTypeRecord = $this->_getBlockTypeRecord($blockType); $blockTypeRecord->fieldId = $blockType->fieldId; $blockTypeRecord->name = $blockType->name; $blockTypeRecord->handle = $blockType->handle; if (!$blockTypeRecord->validate()) { $validates = false; $blockType->addErrors($blockTypeRecord->getErrors()); } foreach ($blockType->getCustomFields() as $field) { // Hack to allow blank field names if (!$field->name) { $field->name = '__blank__'; } $field->validate(); // Make sure the block type handle + field handle combo is unique for the whole field. This prevents us from // worrying about content column conflicts like "a" + "b_c" == "a_b" + "c". if ($blockType->handle && $field->handle) { $blockTypeAndFieldHandle = $blockType->handle . '_' . $field->handle; if (in_array($blockTypeAndFieldHandle, $this->_uniqueBlockTypeAndFieldHandles, true)) { // This error *might* not be entirely accurate, but it's such an edge case that it's probably better // for the error to be worded for the common problem (two duplicate handles within the same block // type). $error = Craft::t('app', '{attribute} “{value}” has already been taken.', [ 'attribute' => Craft::t('app', 'Handle'), 'value' => $field->handle, ]); $field->addError('handle', $error); } else { $this->_uniqueBlockTypeAndFieldHandles[] = $blockTypeAndFieldHandle; } if (!$field->hasErrors('handle')) { // Make sure the handle isn't too long when including the block type handle as individual fields // don't account for it. $maxHandleLength = Craft::$app->getDb()->getSchema()->maxObjectNameLength; $maxHandleLength -= strlen(Craft::$app->getContent()->fieldColumnPrefix . '_'); $maxHandleLength -= strlen($blockType->handle . '_'); $maxHandleLength -= strlen('_' . $field->columnSuffix); $validator = new StringValidator([ 'max' => $maxHandleLength, ]); /** @var Field $field */ $validator->validateAttribute($field, 'handle'); } } if ($field->hasErrors()) { $blockType->hasFieldErrors = true; $validates = false; } } return $validates; } /** * Saves a block type. * * @param MatrixBlockType $blockType The block type to be saved. * @param bool $runValidation Whether the block type should be validated before being saved. * Defaults to `true`. * @return bool * @throws Exception if an error occurs when saving the block type * @throws Throwable if reasons */ public function saveBlockType(MatrixBlockType $blockType, bool $runValidation = true): bool { if ($runValidation && !$blockType->validate()) { return false; } $isNewBlockType = $blockType->getIsNew(); $configPath = ProjectConfig::PATH_MATRIX_BLOCK_TYPES . '.' . $blockType->uid; $configData = $blockType->getConfig(); $field = $blockType->getField(); Craft::$app->getProjectConfig()->set($configPath, $configData, "Save matrix block type “{$blockType->handle}” for parent field “{$field->handle}”"); if ($isNewBlockType) { $blockType->id = Db::idByUid(Table::MATRIXBLOCKTYPES, $blockType->uid); } return true; } /** * Handle block type change * * @param ConfigEvent $event */ public function handleChangedBlockType(ConfigEvent $event): void { $blockTypeUid = $event->tokenMatches[0]; $data = $event->newValue; $previousData = $event->oldValue; // Make sure the field has been synced $fieldId = Db::idByUid(Table::FIELDS, $data['field']); if ($fieldId === null) { Craft::$app->getProjectConfig()->defer($event, [$this, __FUNCTION__]); return; } $fieldsService = Craft::$app->getFields(); $contentService = Craft::$app->getContent(); $transaction = Craft::$app->getDb()->beginTransaction(); // Store the current contexts. $originalContentTable = $contentService->contentTable; $originalFieldContext = $contentService->fieldContext; $originalFieldColumnPrefix = $contentService->fieldColumnPrefix; $originalOldFieldColumnPrefix = $fieldsService->oldFieldColumnPrefix; try { // Get the block type record $blockTypeRecord = $this->_getBlockTypeRecord($blockTypeUid); // Set the basic info on the new block type record $blockTypeRecord->fieldId = $fieldId; $blockTypeRecord->name = $data['name']; $blockTypeRecord->handle = $data['handle']; $blockTypeRecord->sortOrder = $data['sortOrder']; $blockTypeRecord->uid = $blockTypeUid; // Make sure that alterations, if any, occur in the correct context. $contentService->fieldContext = 'matrixBlockType:' . $blockTypeUid; $contentService->fieldColumnPrefix = 'field_' . $blockTypeRecord->handle . '_'; /** @var MatrixField $matrixField */ $matrixField = $fieldsService->getFieldById($blockTypeRecord->fieldId); // Ignore it, if the parent field is not a Matrix field. if ($matrixField instanceof MatrixField) { $contentService->contentTable = $matrixField->contentTable; $fieldsService->oldFieldColumnPrefix = 'field_' . ($blockTypeRecord->getOldAttribute('handle') ?? $data['handle']) . '_'; $oldFields = $previousData['fields'] ?? []; $newFields = $data['fields'] ?? []; // Remove fields that this block type no longer has foreach ($oldFields as $fieldUid => $fieldData) { if (!array_key_exists($fieldUid, $newFields)) { $fieldsService->applyFieldDelete($fieldUid); } } // (Re)save all the fields that now exist for this block. foreach ($newFields as $fieldUid => $fieldData) { $fieldsService->applyFieldSave($fieldUid, $fieldData, 'matrixBlockType:' . $blockTypeUid); } // Refresh the schema cache Craft::$app->getDb()->getSchema()->refresh(); if ( !empty($data['fieldLayouts']) && ($layoutConfig = reset($data['fieldLayouts'])) ) { // Save the field layout $layout = FieldLayout::createFromConfig($layoutConfig); $layout->id = $blockTypeRecord->fieldLayoutId; $layout->type = MatrixBlock::class; $layout->uid = key($data['fieldLayouts']); $fieldsService->saveLayout($layout, false); $blockTypeRecord->fieldLayoutId = $layout->id; } elseif ($blockTypeRecord->fieldLayoutId) { // Delete the field layout $fieldsService->deleteLayoutById($blockTypeRecord->fieldLayoutId); $blockTypeRecord->fieldLayoutId = null; } // Save it $blockTypeRecord->save(false); } $transaction->commit(); } catch (Throwable $e) { $transaction->rollBack(); throw $e; } // Restore the previous contexts. $contentService->fieldContext = $originalFieldContext; $contentService->fieldColumnPrefix = $originalFieldColumnPrefix; $contentService->contentTable = $originalContentTable; $fieldsService->oldFieldColumnPrefix = $originalOldFieldColumnPrefix; // Clear caches unset( $this->_blockTypesById[$blockTypeRecord->id], $this->_blockTypesByFieldId[$blockTypeRecord->fieldId] ); $this->_fetchedAllBlockTypesForFieldId[$blockTypeRecord->fieldId] = false; // Invalidate Matrix block caches Craft::$app->getElements()->invalidateCachesForElementType(MatrixBlock::class); } /** * Deletes a block type. * * @param MatrixBlockType $blockType The block type. * @return bool Whether the block type was deleted successfully. */ public function deleteBlockType(MatrixBlockType $blockType): bool { Craft::$app->getProjectConfig()->remove(ProjectConfig::PATH_MATRIX_BLOCK_TYPES . '.' . $blockType->uid, "Delete matrix block type “{$blockType->handle}” for parent field “{$blockType->getField()->handle}”"); return true; } /** * Handle block type change * * @param ConfigEvent $event * @throws Throwable if reasons */ public function handleDeletedBlockType(ConfigEvent $event): void { $blockTypeUid = $event->tokenMatches[0]; $blockTypeRecord = $this->_getBlockTypeRecord($blockTypeUid); if (!$blockTypeRecord->id) { return; } $transaction = Craft::$app->getDb()->beginTransaction(); try { $blockType = $this->getBlockTypeById($blockTypeRecord->id); if (!$blockType) { return; } // First delete the blocks of this type foreach (Craft::$app->getSites()->getAllSiteIds() as $siteId) { /** @var MatrixBlock[] $blocks */ $blocks = MatrixBlock::find() ->typeId($blockType->id) ->siteId($siteId) ->all(); foreach ($blocks as $block) { Craft::$app->getElements()->deleteElement($block); } } // Set the new contentTable $contentService = Craft::$app->getContent(); $fieldsService = Craft::$app->getFields(); $originalContentTable = $contentService->contentTable; /** @var MatrixField $matrixField */ $matrixField = $fieldsService->getFieldById($blockType->fieldId); // Ignore it, if the parent field is not a Matrix field. if ($matrixField instanceof MatrixField) { $contentService->contentTable = $matrixField->contentTable; // Set the new fieldColumnPrefix + oldFieldColumnPrefix $originalFieldColumnPrefix = $contentService->fieldColumnPrefix; $originalOldFieldColumnPrefix = $fieldsService->oldFieldColumnPrefix; $contentService->fieldColumnPrefix = "field_{$blockType->handle}_"; $fieldsService->oldFieldColumnPrefix = "field_{$blockType->handle}_"; // Now delete the block type fields foreach ($blockType->getCustomFields() as $field) { $fieldsService->deleteField($field); } // Restore the contentTable and the fieldColumnPrefix to original values. $contentService->contentTable = $originalContentTable; $contentService->fieldColumnPrefix = $originalFieldColumnPrefix; $fieldsService->oldFieldColumnPrefix = $originalOldFieldColumnPrefix; // Delete the field layout $fieldLayoutId = (new Query()) ->select(['fieldLayoutId']) ->from([Table::MATRIXBLOCKTYPES]) ->where(['id' => $blockTypeRecord->id]) ->scalar(); // Delete the field layout if ($fieldLayoutId) { $fieldsService->deleteLayoutById($fieldLayoutId); } // Finally delete the actual block type Db::delete(Table::MATRIXBLOCKTYPES, [ 'id' => $blockTypeRecord->id, ]); } $transaction->commit(); } catch (Throwable $e) { $transaction->rollBack(); throw $e; } // Clear caches unset( $this->_blockTypesById[$blockTypeRecord->id], $this->_blockTypesByFieldId[$blockTypeRecord->fieldId], $this->_blockTypeRecordsById[$blockTypeRecord->id] ); $this->_fetchedAllBlockTypesForFieldId[$blockTypeRecord->fieldId] = false; // Invalidate Matrix block caches Craft::$app->getElements()->invalidateCachesForElementType(MatrixBlock::class); } /** * Validates a Matrix field's settings. * * If the settings don’t validate, any validation errors will be stored on the settings model. * * @param MatrixField $matrixField The Matrix field * @return bool Whether the settings validated. */ public function validateFieldSettings(MatrixField $matrixField): bool { $validates = true; $this->_uniqueBlockTypeAndFieldHandles = []; $uniqueAttributes = ['name', 'handle']; $uniqueAttributeValues = []; foreach ($matrixField->getBlockTypes() as $blockType) { if (!$this->validateBlockType($blockType)) { // Don't break out of the loop because we still want to get validation errors for the remaining block // types. $validates = false; } // Do our own unique name/handle validation, since the DB-based validation can't be trusted when saving // multiple records at once foreach ($uniqueAttributes as $attribute) { $value = $blockType->$attribute; if ($value && (!isset($uniqueAttributeValues[$attribute]) || !in_array($value, $uniqueAttributeValues[$attribute], true))) { $uniqueAttributeValues[$attribute][] = $value; } else { $blockType->addError($attribute, Craft::t('app', '{attribute} “{value}” has already been taken.', [ 'attribute' => $blockType->getAttributeLabel($attribute), 'value' => Html::encode($value), ])); $validates = false; } } } return $validates; } /** * Saves a Matrix field's settings. * * @param MatrixField $matrixField The Matrix field * @param bool $validate Whether the settings should be validated before being saved. * @return bool Whether the settings saved successfully. * @throws Throwable if reasons */ public function saveSettings(MatrixField $matrixField, bool $validate = true): bool { if (!isset($matrixField->contentTable)) { throw new Exception('Unable to save a Matrix field’s settings without knowing its content table.'); } if ($validate && !$this->validateFieldSettings($matrixField)) { return false; } $db = Craft::$app->getDb(); $transaction = $db->beginTransaction(); try { // Do we need to create/rename the content table? if (!$db->tableExists($matrixField->contentTable)) { $oldContentTable = $matrixField->oldSettings['contentTable'] ?? null; if ($oldContentTable && $db->tableExists($oldContentTable)) { Db::renameTable($oldContentTable, $matrixField->contentTable); } else { $this->_createContentTable($matrixField->contentTable); } } // Only make block type changes if we're not in the middle of applying external changes if (!Craft::$app->getProjectConfig()->getIsApplyingExternalChanges()) { // Delete the old block types first, in case there's a handle conflict with one of the new ones $oldBlockTypes = $this->getBlockTypesByFieldId($matrixField->id); $oldBlockTypesById = []; foreach ($oldBlockTypes as $blockType) { $oldBlockTypesById[$blockType->id] = $blockType; } foreach ($matrixField->getBlockTypes() as $blockType) { if (!$blockType->getIsNew()) { unset($oldBlockTypesById[$blockType->id]); } } foreach ($oldBlockTypesById as $blockType) { $this->deleteBlockType($blockType); } // Save the new ones $sortOrder = 0; $originalContentTable = Craft::$app->getContent()->contentTable; Craft::$app->getContent()->contentTable = $matrixField->contentTable; foreach ($matrixField->getBlockTypes() as $blockType) { $sortOrder++; $blockType->fieldId = $matrixField->id; $blockType->sortOrder = $sortOrder; $this->saveBlockType($blockType, false); } Craft::$app->getContent()->contentTable = $originalContentTable; } $transaction->commit(); } catch (Throwable $e) { $transaction->rollBack(); throw $e; } // Clear caches unset( $this->_blockTypesByFieldId[$matrixField->id], $this->_fetchedAllBlockTypesForFieldId[$matrixField->id] ); return true; } /** * Deletes a Matrix field. * * @param MatrixField $matrixField The Matrix field. * @return bool Whether the field was deleted successfully. * @throws Throwable */ public function deleteMatrixField(MatrixField $matrixField): bool { // Clear the schema cache $db = Craft::$app->getDb(); $db->getSchema()->refresh(); $transaction = $db->beginTransaction(); try { $originalContentTable = Craft::$app->getContent()->contentTable; Craft::$app->getContent()->contentTable = $matrixField->contentTable; // Delete the block types $blockTypes = $this->getBlockTypesByFieldId($matrixField->id); foreach ($blockTypes as $blockType) { $this->deleteBlockType($blockType); } // Drop the content table $db->createCommand() ->dropTable($matrixField->contentTable) ->execute(); Craft::$app->getContent()->contentTable = $originalContentTable; $transaction->commit(); return true; } catch (Throwable $e) { $transaction->rollBack(); throw $e; } } /** * Defines a new Matrix content table name. * * @param MatrixField $field * @return string * @since 3.0.23 */ public function defineContentTableName(MatrixField $field): string { $baseName = 'matrixcontent_' . strtolower($field->handle); $db = Craft::$app->getDb(); $i = -1; do { $i++; $name = '{{%' . $baseName . ($i !== 0 ? '_' . $i : '') . '}}'; } while ($name !== ($field->contentTable ?? null) && $db->tableExists($name)); return $name; } /** * Returns a block by its ID. * * @param int $blockId The Matrix block’s ID. * @param int|null $siteId The site ID to return. Defaults to the current site. * @return MatrixBlock|null The Matrix block, or `null` if it didn’t exist. */ public function getBlockById(int $blockId, ?int $siteId = null): ?MatrixBlock { return Craft::$app->getElements()->getElementById($blockId, MatrixBlock::class, $siteId); } /** * Saves a Matrix field. * * @param MatrixField $field The Matrix field * @param ElementInterface $owner The element the field is associated with * @throws Throwable if reasons */ public function saveField(MatrixField $field, ElementInterface $owner): void { $elementsService = Craft::$app->getElements(); /** @var MatrixBlockQuery|Collection $value */ $value = $owner->getFieldValue($field->handle); if ($value instanceof Collection) { $blocks = $value->all(); $saveAll = true; } else { $blocks = $value->getCachedResult(); if ($blocks !== null) { $saveAll = false; } else { $blocks = (clone $value)->status(null)->all(); $saveAll = true; } } $blockIds = []; $collapsedBlockIds = []; $sortOrder = 0; $transaction = Craft::$app->getDb()->beginTransaction(); try { /** @var MatrixBlock[] $blocks */ foreach ($blocks as $block) { $sortOrder++; if ($saveAll || !$block->id || $block->dirty) { $block->setOwner($owner); // If the block already has an ID and primary owner ID, don't reassign it if (!$block->id || !$block->primaryOwnerId) { $block->primaryOwnerId = $owner->id; } $block->sortOrder = $sortOrder; $elementsService->saveElement($block, false); // If this is a draft, we can shed the draft data now if ($block->getIsDraft()) { $canonicalBlockId = $block->getCanonicalId(); Craft::$app->getDrafts()->removeDraftData($block); Db::delete(Table::MATRIXBLOCKS_OWNERS, [ 'blockId' => $canonicalBlockId, 'ownerId' => $owner->id, ]); } } elseif ((int)$block->sortOrder !== $sortOrder) { // Just update its sortOrder $block->sortOrder = $sortOrder; Db::update(Table::MATRIXBLOCKS_OWNERS, [ 'sortOrder' => $sortOrder, ], [ 'blockId' => $block->id, 'ownerId' => $owner->id, ], [], false); } $blockIds[] = $block->id; // Tell the browser to collapse this block? if ($block->collapsed) { $collapsedBlockIds[] = $block->id; } } // Delete any blocks that shouldn't be there anymore $this->_deleteOtherBlocks($field, $owner, $blockIds); // Should we duplicate the blocks to other sites? if ( $field->propagationMethod !== MatrixField::PROPAGATION_METHOD_ALL && ($owner->propagateAll || !empty($owner->newSiteIds)) ) { // Find the owner's site IDs that *aren't* supported by this site's Matrix blocks $ownerSiteIds = array_map( fn(array $siteInfo) => $siteInfo['siteId'], ElementHelper::supportedSitesForElement($owner), ); $fieldSiteIds = $this->getSupportedSiteIds($field->propagationMethod, $owner, $field->propagationKeyFormat); $otherSiteIds = array_diff($ownerSiteIds, $fieldSiteIds); // If propagateAll isn't set, only deal with sites that the element was just propagated to for the first time if (!$owner->propagateAll) { $preexistingOtherSiteIds = array_diff($otherSiteIds, $owner->newSiteIds); $otherSiteIds = array_intersect($otherSiteIds, $owner->newSiteIds); } else { $preexistingOtherSiteIds = []; } if (!empty($otherSiteIds)) { // Get the owner element across each of those sites $localizedOwners = $owner::find() ->drafts($owner->getIsDraft()) ->provisionalDrafts($owner->isProvisionalDraft) ->revisions($owner->getIsRevision()) ->id($owner->id) ->siteId($otherSiteIds) ->status(null) ->all(); // Duplicate Matrix blocks, ensuring we don't process the same blocks more than once $handledSiteIds = []; if ($value instanceof MatrixBlockQuery) { $cachedQuery = (clone $value)->status(null); $cachedQuery->setCachedResult($blocks); $owner->setFieldValue($field->handle, $cachedQuery); } foreach ($localizedOwners as $localizedOwner) { // Make sure we haven't already duplicated blocks for this site, via propagation from another site if (isset($handledSiteIds[$localizedOwner->siteId])) { continue; } // Find all of the field’s supported sites shared with this target $sourceSupportedSiteIds = $this->getSupportedSiteIds($field->propagationMethod, $localizedOwner, $field->propagationKeyFormat); // Do blocks in this target happen to share supported sites with a preexisting site? if ( !empty($preexistingOtherSiteIds) && !empty($sharedPreexistingOtherSiteIds = array_intersect($preexistingOtherSiteIds, $sourceSupportedSiteIds)) && $preexistingLocalizedOwner = $owner::find() ->drafts($owner->getIsDraft()) ->provisionalDrafts($owner->isProvisionalDraft) ->revisions($owner->getIsRevision()) ->id($owner->id) ->siteId($sharedPreexistingOtherSiteIds) ->status(null) ->one() ) { // Just resave Matrix blocks for that one site, and let them propagate over to the new site(s) from there $this->saveField($field, $preexistingLocalizedOwner); } else { // Duplicate the blocks, but **don't track** the duplications, so the edit page doesn’t think // its blocks have been replaced by the other sites’ blocks $this->duplicateBlocks($field, $owner, $localizedOwner, trackDuplications: false, force: true); } // Make sure we don't duplicate blocks for any of the sites that were just propagated to foreach ($sourceSupportedSiteIds as $siteId) { $handledSiteIds[$siteId] = true; } } if ($value instanceof MatrixBlockQuery) { $owner->setFieldValue($field->handle, $value); } } } $transaction->commit(); } catch (Throwable $e) { $transaction->rollBack(); throw $e; } // Tell the browser to collapse any new block IDs if (!Craft::$app->getRequest()->getIsConsoleRequest() && !Craft::$app->getResponse()->isSent && !empty($collapsedBlockIds)) { Craft::$app->getSession()->addAssetBundleFlash(MatrixAsset::class); foreach ($collapsedBlockIds as $blockId) { Craft::$app->getSession()->addJsFlash('Craft.MatrixInput.rememberCollapsedBlockId(' . $blockId . ');', View::POS_END); } } } /** * Duplicates Matrix blocks from one owner element to another. * * @param MatrixField $field The Matrix field to duplicate blocks for * @param ElementInterface $source The source element blocks should be duplicated from * @param ElementInterface $target The target element blocks should be duplicated to * @param bool $checkOtherSites Whether to duplicate blocks for the source element’s other supported sites * @param bool $deleteOtherBlocks Whether to delete any blocks that belong to the element, which weren’t included in the duplication * @param bool $trackDuplications whether to keep track of the duplications from [[\craft\services\Elements::$duplicatedElementIds]] * and [[\craft\services\Elements::$duplicatedElementSourceIds]] * @param bool $force Whether to force duplication, even if it looks like only the block ownership was duplicated * @throws Throwable if reasons * @since 3.2.0 */ public function duplicateBlocks( MatrixField $field, ElementInterface $source, ElementInterface $target, bool $checkOtherSites = false, bool $deleteOtherBlocks = true, bool $trackDuplications = true, bool $force = false, ): void { $elementsService = Craft::$app->getElements(); /** @var MatrixBlockQuery|Collection $value */ $value = $source->getFieldValue($field->handle); if ($value instanceof Collection) { $blocks = $value->all(); } else { $blocks = $value->getCachedResult() ?? (clone $value)->status(null)->all(); } $newBlockIds = []; $transaction = Craft::$app->getDb()->beginTransaction(); try { $setCanonicalId = $target->getIsDerivative() && $target->getCanonical()->id !== $target->id; /** @var MatrixBlock[] $blocks */ foreach ($blocks as $block) { $newAttributes = [ // Only set the canonicalId if the target owner element is a derivative // and if the target's canonical element is not the same as target element, see // https://app.frontapp.com/open/msg_ukaoki1?key=U6zkE_S6_ApMXn3ntPMwUxSLe0sUPsmY for more info 'canonicalId' => $setCanonicalId ? $block->id : null, 'primaryOwnerId' => $target->id, 'owner' => $target, 'siteId' => $target->siteId, 'propagating' => false, ]; if ($target->updatingFromDerivative && $block->getIsDerivative()) { if ( ElementHelper::isRevision($source) || !empty($target->newSiteIds) || (!$source::trackChanges() || $source->isFieldModified($field->handle, true)) ) { $newBlockId = $elementsService->updateCanonicalElement($block, $newAttributes)->id; } else { $newBlockId = $block->getCanonicalId(); } } elseif (!$force && $block->primaryOwnerId === $target->id) { // Only the block ownership was duplicated, so just update its sort order for the target element // (use upsert in case the row doesn’t exist though) Db::upsert(Table::MATRIXBLOCKS_OWNERS, [ 'blockId' => $block->id, 'ownerId' => $target->id, 'sortOrder' => $block->sortOrder, ], [ 'sortOrder' => $block->sortOrder, ], updateTimestamp: false); $newBlockId = $block->id; } else { $newBlockId = $elementsService->duplicateElement($block, $newAttributes, trackDuplication: $trackDuplications)->id; } $newBlockIds[] = $newBlockId; } if ($deleteOtherBlocks) { // Delete any blocks that shouldn't be there anymore $this->_deleteOtherBlocks($field, $target, $newBlockIds); } $transaction->commit(); } catch (Throwable $e) { $transaction->rollBack(); throw $e; } // Duplicate blocks for other sites as well? if ($checkOtherSites && $field->propagationMethod !== MatrixField::PROPAGATION_METHOD_ALL) { // Find the target's site IDs that *aren't* supported by this site's Matrix blocks $targetSiteIds = array_map( fn(array $siteInfo) => $siteInfo['siteId'], ElementHelper::supportedSitesForElement($target), ); $fieldSiteIds = $this->getSupportedSiteIds($field->propagationMethod, $target, $field->propagationKeyFormat); $otherSiteIds = array_diff($targetSiteIds, $fieldSiteIds); if (!empty($otherSiteIds)) { // Get the original element and duplicated element for each of those sites $otherSources = $target::find() ->drafts($source->getIsDraft()) ->provisionalDrafts($source->isProvisionalDraft) ->revisions($source->getIsRevision()) ->id($source->id) ->siteId($otherSiteIds) ->status(null) ->all(); $otherTargets = $target::find() ->drafts($target->getIsDraft()) ->provisionalDrafts($target->isProvisionalDraft) ->revisions($target->getIsRevision()) ->id($target->id) ->siteId($otherSiteIds) ->status(null) ->indexBy('siteId') ->all(); // Duplicate Matrix blocks, ensuring we don't process the same blocks more than once $handledSiteIds = []; foreach ($otherSources as $otherSource) { // Make sure the target actually exists for this site if (!isset($otherTargets[$otherSource->siteId])) { continue; } // Make sure we haven't already duplicated blocks for this site, via propagation from another site if (in_array($otherSource->siteId, $handledSiteIds, false)) { continue; } $otherTargets[$otherSource->siteId]->updatingFromDerivative = $target->updatingFromDerivative; $this->duplicateBlocks($field, $otherSource, $otherTargets[$otherSource->siteId]); // Make sure we don't duplicate blocks for any of the sites that were just propagated to $sourceSupportedSiteIds = $this->getSupportedSiteIds($field->propagationMethod, $otherSource, $field->propagationKeyFormat); $handledSiteIds = array_merge($handledSiteIds, $sourceSupportedSiteIds); } } } } /** * Duplicates block ownership relations for a new draft element. * * @param MatrixField $field The Matrix field * @param ElementInterface $canonical The canonical element * @param ElementInterface $draft The draft element * @since 4.0.0 */ public function duplicateOwnership(MatrixField $field, ElementInterface $canonical, ElementInterface $draft): void { if (!$canonical->getIsCanonical()) { throw new InvalidArgumentException('The source element must be canonical.'); } if (!$draft->getIsDraft()) { throw new InvalidArgumentException('The target element must be a draft.'); } $blocksTable = Table::MATRIXBLOCKS; $ownersTable = Table::MATRIXBLOCKS_OWNERS; Craft::$app->getDb()->createCommand(<<<SQL INSERT INTO $ownersTable ([[blockId]], [[ownerId]], [[sortOrder]]) SELECT [[o.blockId]], '$draft->id', [[o.sortOrder]] FROM $ownersTable AS [[o]] INNER JOIN $blocksTable AS [[b]] ON [[b.id]] = [[o.blockId]] AND [[b.primaryOwnerId]] = '$canonical->id' AND [[b.fieldId]] = '$field->id' WHERE [[o.ownerId]] = '$canonical->id' SQL )->execute(); } /** * Creates revisions for all the blocks that belong to the given canonical element, and assigns those * revisions to the given owner revision. * * @param MatrixField $field The Matrix field * @param ElementInterface $canonical The canonical element * @param ElementInterface $revision The revision element * @since 4.0.0 */ public function createRevisionBlocks(MatrixField $field, ElementInterface $canonical, ElementInterface $revision): void { // Only fetch blocks in the sites the owner element supports $siteIds = array_map( fn(array $siteInfo) => $siteInfo['siteId'], ElementHelper::supportedSitesForElement($canonical), ); /** @var MatrixBlock[] $blocks */ $blocks = MatrixBlock::find() ->ownerId($canonical->id) ->fieldId($field->id) ->siteId($siteIds) ->preferSites([$canonical->siteId]) ->unique() ->status(null) ->all(); $revisionsService = Craft::$app->getRevisions(); $ownershipData = []; foreach ($blocks as $block) { $blockRevisionId = $revisionsService->createRevision($block, null, null, [ 'primaryOwnerId' => $revision->id, 'saveOwnership' => false, ]); $ownershipData[] = [$blockRevisionId, $revision->id, $block->sortOrder]; } Db::batchInsert(Table::MATRIXBLOCKS_OWNERS, ['blockId', 'ownerId', 'sortOrder'], $ownershipData); } /** * Merges recent canonical Matrix block changes into the given Matrix field’s blocks. * * @param MatrixField $field The Matrix field * @param ElementInterface $owner The element the field is associated with * @since 3.7.0 */ public function mergeCanonicalChanges(MatrixField $field, ElementInterface $owner): void { // Get the owner across all sites $localizedOwners = $owner::find() ->id($owner->id ?: false) ->siteId(['not', $owner->siteId]) ->drafts($owner->getIsDraft()) ->provisionalDrafts($owner->isProvisionalDraft) ->revisions($owner->getIsRevision()) ->status(null) ->ignorePlaceholders() ->indexBy('siteId') ->all(); $localizedOwners[$owner->siteId] = $owner; // Get the canonical owner across all sites $canonicalOwners = $owner::find() ->id($owner->getCanonicalId()) ->siteId(array_keys($localizedOwners)) ->status(null) ->ignorePlaceholders() ->all(); $elementsService = Craft::$app->getElements(); $handledSiteIds = []; foreach ($canonicalOwners as $canonicalOwner) { if (isset($handledSiteIds[$canonicalOwner->siteId])) { continue; } // Get all the canonical owner’s blocks, including soft-deleted ones /** @var MatrixBlock[] $canonicalBlocks */ $canonicalBlocks = MatrixBlock::find() ->fieldId($field->id) ->primaryOwnerId($canonicalOwner->id) ->siteId($canonicalOwner->siteId) ->status(null) ->trashed(null) ->ignorePlaceholders() ->all(); // Get all the derivative owner’s blocks, so we can compare /** @var MatrixBlock[] $derivativeBlocks */ $derivativeBlocks = MatrixBlock::find() ->fieldId($field->id) ->primaryOwnerId($owner->id) ->siteId($canonicalOwner->siteId) ->status(null) ->trashed(null) ->ignorePlaceholders() ->indexBy('canonicalId') ->all(); foreach ($canonicalBlocks as $canonicalBlock) { if (isset($derivativeBlocks[$canonicalBlock->id])) { $derivativeBlock = $derivativeBlocks[$canonicalBlock->id]; // Has it been soft-deleted? if ($canonicalBlock->trashed) { // Delete the derivative block too, unless any changes were made to it if ($derivativeBlock->dateUpdated == $derivativeBlock->dateCreated) { $elementsService->deleteElement($derivativeBlock); } } elseif (!$derivativeBlock->trashed && ElementHelper::isOutdated($derivativeBlock)) { // Merge the upstream changes into the derivative block $elementsService->mergeCanonicalChanges($derivativeBlock); } } elseif (!$canonicalBlock->trashed && $canonicalBlock->dateCreated > $owner->dateCreated) { // This is a new block, so duplicate it into the derivative owner $elementsService->duplicateElement($canonicalBlock, [ 'canonicalId' => $canonicalBlock->id, 'primaryOwnerId' => $owner->id, 'owner' => $localizedOwners[$canonicalBlock->siteId], 'siteId' => $canonicalBlock->siteId, 'propagating' => false, ]); } } // Keep track of the sites we've already covered $siteIds = $this->getSupportedSiteIds($field->propagationMethod, $canonicalOwner, $field->propagationKeyFormat); foreach ($siteIds as $siteId) { $handledSiteIds[$siteId] = true; } } } /** * Returns the site IDs that are supported by Matrix blocks for the given propagation method and owner element. * * @param string $propagationMethod * @param ElementInterface $owner * @param string|null $propagationKeyFormat * @return int[] * @since 3.3.18 */ public function getSupportedSiteIds(string $propagationMethod, ElementInterface $owner, ?string $propagationKeyFormat = null): array { /** @var Site[] $allSites */ $allSites = ArrayHelper::index(Craft::$app->getSites()->getAllSites(), 'id'); $ownerSiteIds = array_map( fn(array $siteInfo) => $siteInfo['siteId'], ElementHelper::supportedSitesForElement($owner), ); $siteIds = []; $view = Craft::$app->getView(); $elementsService = Craft::$app->getElements(); if ($propagationMethod === MatrixField::PROPAGATION_METHOD_CUSTOM && $propagationKeyFormat !== null) { $propagationKey = $view->renderObjectTemplate($propagationKeyFormat, $owner); } foreach ($ownerSiteIds as $siteId) { switch ($propagationMethod) { case MatrixField::PROPAGATION_METHOD_NONE: $include = $siteId == $owner->siteId; break; case MatrixField::PROPAGATION_METHOD_SITE_GROUP: $include = $allSites[$siteId]->groupId == $allSites[$owner->siteId]->groupId; break; case MatrixField::PROPAGATION_METHOD_LANGUAGE: $include = $allSites[$siteId]->language == $allSites[$owner->siteId]->language; break; case MatrixField::PROPAGATION_METHOD_CUSTOM: if (!isset($propagationKey)) { $include = true; } else { $siteOwner = $elementsService->getElementById($owner->id, get_class($owner), $siteId); $include = $siteOwner && $propagationKey === $view->renderObjectTemplate($propagationKeyFormat, $siteOwner); } break; default: $include = true; break; } if ($include) { $siteIds[] = $siteId; } } return $siteIds; } /** * Returns a Query object prepped for retrieving block types. * * @return Query */ private function _createBlockTypeQuery(): Query { return (new Query()) ->select([ 'bt.id', 'bt.fieldId', 'bt.fieldLayoutId', 'bt.name', 'bt.handle', 'bt.sortOrder', 'bt.uid', ]) ->from(['bt' => Table::MATRIXBLOCKTYPES]) ->orderBy(['bt.sortOrder' => SORT_ASC]); } /** * Returns a block type record by its model or UID or creates a new one. * * @param string|MatrixBlockType $blockType * @return MatrixBlockTypeRecord * @throws MatrixBlockTypeNotFoundException if $blockType->id is invalid */ private function _getBlockTypeRecord(string|MatrixBlockType $blockType): MatrixBlockTypeRecord { if (is_string($blockType)) { $blockTypeRecord = MatrixBlockTypeRecord::findOne(['uid' => $blockType]) ?? new MatrixBlockTypeRecord(); if (!$blockTypeRecord->getIsNewRecord()) { $this->_blockTypeRecordsById[$blockTypeRecord->id] = $blockTypeRecord; } return $blockTypeRecord; } if ($blockType->getIsNew()) { return new MatrixBlockTypeRecord(); } if (isset($this->_blockTypeRecordsById[$blockType->id])) { return $this->_blockTypeRecordsById[$blockType->id]; } $blockTypeRecord = MatrixBlockTypeRecord::findOne($blockType->id); if ($blockTypeRecord === null) { throw new MatrixBlockTypeNotFoundException('Invalid block type ID: ' . $blockType->id); } return $this->_blockTypeRecordsById[$blockType->id] = $blockTypeRecord; } /** * Creates the content table for a Matrix field. * * @param string $tableName */ private function _createContentTable(string $tableName): void { $migration = new CreateMatrixContentTable([ 'tableName' => $tableName, ]); ob_start(); $migration->up(); ob_end_clean(); } /** * Deletes blocks from an owner element * * @param MatrixField $field The Matrix field * @param ElementInterface $owner The owner element * @param int[] $except Block IDs that should be left alone */ private function _deleteOtherBlocks(MatrixField $field, ElementInterface $owner, array $except): void { /** @var MatrixBlock[] $blocks */ $blocks = MatrixBlock::find() ->ownerId($owner->id) ->fieldId($field->id) ->status(null) ->siteId($owner->siteId) ->andWhere(['not', ['elements.id' => $except]]) ->all(); $elementsService = Craft::$app->getElements(); $deleteOwnership = []; foreach ($blocks as $block) { if ($block->primaryOwnerId === $owner->id) { $elementsService->deleteElement($block); } else { // Just delete the ownership relation $deleteOwnership[] = $block->id; } } if ($deleteOwnership) { Db::delete(Table::MATRIXBLOCKS_OWNERS, [ 'blockId' => $deleteOwnership, 'ownerId' => $owner->id, ]); } } }
/srv/users/craft4/apps/craft4-newsite-space/vendor/craftcms/././cms/src/./services/./Matrix.php