diff --git a/src/Assets/AssetReferenceUpdater.php b/src/Assets/AssetReferenceUpdater.php index e0e86c1c77f..4f1e5ba59bd 100644 --- a/src/Assets/AssetReferenceUpdater.php +++ b/src/Assets/AssetReferenceUpdater.php @@ -3,7 +3,6 @@ namespace Statamic\Assets; use Statamic\Data\DataReferenceUpdater; -use Statamic\Facades\AssetContainer; use Statamic\Support\Arr; class AssetReferenceUpdater extends DataReferenceUpdater @@ -33,304 +32,37 @@ public function filterByContainer(string $container) */ protected function recursivelyUpdateFields($fields, $dottedPrefix = null) { - $this - ->updateAssetsFieldValues($fields, $dottedPrefix) - ->updateLinkFieldValues($fields, $dottedPrefix) - ->updateBardFieldValues($fields, $dottedPrefix) - ->updateMarkdownFieldValues($fields, $dottedPrefix) - ->updateNestedFieldValues($fields, $dottedPrefix); - } - - /** - * Update assets field values. - * - * @param \Illuminate\Support\Collection $fields - * @param null|string $dottedPrefix - * @return $this - */ - protected function updateAssetsFieldValues($fields, $dottedPrefix) - { - $fields - ->filter(function ($field) { - return $field->type() === 'assets' - && $this->getConfiguredAssetsFieldContainer($field) === $this->container; - }) + $this->fieldsWithReferenceUpdates($fields) ->each(function ($field) use ($dottedPrefix) { - $this->hasStringValue($field, $dottedPrefix) - ? $this->updateStringValue($field, $dottedPrefix) - : $this->updateArrayValue($field, $dottedPrefix); - }); - - return $this; - } - - /** - * Update link field values. - * - * @param \Illuminate\Support\Collection $fields - * @param null|string $dottedPrefix - * @return $this - */ - protected function updateLinkFieldValues($fields, $dottedPrefix) - { - $fields - ->filter(function ($field) { - return $field->type() === 'link' - && $field->get('container') === $this->container; - }) - ->each(function ($field) use ($dottedPrefix) { - $this->updateStatamicUrlsInLinkValue($field, $dottedPrefix); - }); - - return $this; - } - - /** - * Update bard field values. - * - * @param \Illuminate\Support\Collection $fields - * @param null|string $dottedPrefix - * @return $this - */ - protected function updateBardFieldValues($fields, $dottedPrefix) - { - $fields - ->filter(function ($field) { - return $field->type() === 'bard' - && $field->get('container') === $this->container; - }) - ->each(function ($field) use ($dottedPrefix) { - $this->hasStringValue($field, $dottedPrefix) - ? $this->updateStatamicUrlsInStringValue($field, $dottedPrefix) - : $this->updateStatamicUrlsInArrayValue($field, $dottedPrefix); - }); - - return $this; - } - - /** - * Update markdown field values. - * - * @param \Illuminate\Support\Collection $fields - * @param null|string $dottedPrefix - * @return $this - */ - protected function updateMarkdownFieldValues($fields, $dottedPrefix) - { - $fields - ->filter(function ($field) { - return $field->type() === 'markdown' - && $field->get('container') === $this->container; - }) - ->each(function ($field) use ($dottedPrefix) { - $this->updateStatamicUrlsInStringValue($field, $dottedPrefix); - }); - - return $this; - } - - /** - * Get configured assets field container, or implied asset container if only one exists. - * - * @param \Statamic\Fields\Field $field - * @return string - */ - protected function getConfiguredAssetsFieldContainer($field) - { - if ($container = $field->get('container')) { - return $container; - } - - $containers = AssetContainer::all(); - - return $containers->count() === 1 - ? $containers->first()->handle() - : null; - } - - /** - * Update `statamic://` urls in string value on item. - * - * @param \Statamic\Fields\Field $field - * @param null|string $dottedPrefix - */ - protected function updateStatamicUrlsInStringValue($field, $dottedPrefix) - { - $data = $this->item->data()->all(); - - $dottedKey = $dottedPrefix.$field->handle(); - - $originalValue = $value = Arr::get($data, $dottedKey); - - if (! $originalValue) { - return; - } - - $value = preg_replace_callback('/([("])(statamic:\/\/[^()"]*::)([^)"]*)([)"])/im', function ($matches) { - $newValue = $this->isRemovingValue() ? '' : $matches[2].$this->newValue; - - return $matches[3] === $this->originalValue - ? $matches[1].$newValue.$matches[4] - : $matches[0]; - }, $value); - - if ($originalValue === $value) { - return; - } - - Arr::set($data, $dottedKey, $value); - - $this->item->data($data); - - $this->updated = true; - } - - /** - * Update asset references in link values. - * - * @param \Statamic\Fields\Field $field - * @param null|string $dottedPrefix - */ - private function updateStatamicUrlsInLinkValue($field, $dottedPrefix) - { - $data = $this->item->data()->all(); - - $dottedKey = $dottedPrefix.$field->handle(); - - $originalValue = $value = Arr::get($data, $dottedKey); - - if (! $originalValue) { - return; - } - - if ($value !== "asset::{$this->container}::{$this->originalValue}") { - return; - } - - $newValue = $this->isRemovingValue() - ? null - : "asset::{$this->container}::{$this->newValue}"; - - if ($originalValue === $newValue) { - return; - } - - if ($this->isRemovingValue()) { - Arr::forget($data, $dottedKey); - } else { - Arr::set($data, $dottedKey, $newValue); - } - - $this->item->data($data); - - $this->updated = true; - } - - /** - * Update asset references in bard set on item. - * - * @param \Statamic\Fields\Field $field - * @param null|string $dottedPrefix - */ - protected function updateStatamicUrlsInArrayValue($field, $dottedPrefix) - { - $this->updateStatamicUrlsInImageNodes($field, $dottedPrefix); - $this->updateStatamicUrlsInLinkNodes($field, $dottedPrefix); - } - - /** - * Update asset references in bard image nodes. - * - * @param \Statamic\Fields\Field $field - * @param null|string $dottedPrefix - */ - private function updateStatamicUrlsInImageNodes($field, $dottedPrefix) - { - $data = $this->item->data()->all(); - - $dottedKey = $dottedPrefix.$field->handle(); - - $bardPayload = Arr::get($data, $dottedKey, []); - - if (! $bardPayload) { - return; - } - - $changed = collect(Arr::dot($bardPayload)) - ->filter(function ($value, $key) { - return preg_match('/(.*)\.(type)/', $key) && $value === 'image'; - }) - ->mapWithKeys(function ($value, $key) use ($bardPayload) { - $key = str_replace('.type', '.attrs.src', $key); - - return [$key => Arr::get($bardPayload, $key)]; - }) - ->filter(function ($value) { - return $value === "asset::{$this->container}::{$this->originalValue}"; - }) - ->map(function ($value) { - return "asset::{$this->container}::{$this->newValue}"; - }) - ->each(function ($value, $key) use (&$bardPayload) { - Arr::set($bardPayload, $key, $this->isRemovingValue() ? '' : $value); - }); - - if ($changed->isEmpty()) { - return; - } - - Arr::set($data, $dottedKey, $bardPayload); - - $this->item->data($data); - - $this->updated = true; - } - - /** - * Update asset references in bard link nodes. - * - * @param \Statamic\Fields\Field $field - * @param null|string $dottedPrefix - */ - private function updateStatamicUrlsInLinkNodes($field, $dottedPrefix) - { - $data = $this->item->data()->all(); - - $dottedKey = $dottedPrefix.$field->handle(); - - $bardPayload = Arr::get($data, $dottedKey, []); - - if (! $bardPayload) { - return; - } - - $changed = collect(Arr::dot($bardPayload)) - ->filter(function ($value, $key) { - return preg_match('/(.*)\.(type)/', $key) && $value === 'link'; - }) - ->mapWithKeys(function ($value, $key) use ($bardPayload) { - $key = str_replace('.type', '.attrs.href', $key); - - return [$key => Arr::get($bardPayload, $key)]; - }) - ->filter(function ($value) { - return $value === "statamic://asset::{$this->container}::{$this->originalValue}"; - }) - ->map(function ($value) { - return "statamic://asset::{$this->container}::{$this->newValue}"; - }) - ->each(function ($value, $key) use (&$bardPayload) { - Arr::set($bardPayload, $key, $this->isRemovingValue() ? '' : $value); + $data = $this->item->data()->all(); + $dottedKey = $dottedPrefix.$field->handle(); + $oldData = Arr::get($data, $dottedKey); + + if ($oldData === null) { + return; + } + + $newData = $field->fieldtype()->replaceAssetReferences( + $oldData, + $this->newValue, + $this->originalValue, + $this->container + ); + + if ($oldData === $newData) { + return; + } + + if ($newData === null && $this->isRemovingValue()) { + Arr::forget($data, $dottedKey); + } else { + Arr::set($data, $dottedKey, $newData); + } + + $this->item->data($data); + $this->updated = true; }); - if ($changed->isEmpty()) { - return; - } - - Arr::set($data, $dottedKey, $bardPayload); - - $this->item->data($data); - - $this->updated = true; + $this->updateNestedFieldValues($fields, $dottedPrefix); } } diff --git a/src/Data/DataReferenceUpdater.php b/src/Data/DataReferenceUpdater.php index 8c5cc343a5b..1eaaf2974ff 100644 --- a/src/Data/DataReferenceUpdater.php +++ b/src/Data/DataReferenceUpdater.php @@ -2,7 +2,8 @@ namespace Statamic\Data; -use Statamic\Fields\Fields; +use Statamic\Facades\Blink; +use Statamic\Fieldtypes\UpdatesReferences; use Statamic\Git\Subscriber as GitSubscriber; use Statamic\Support\Arr; @@ -28,6 +29,11 @@ abstract class DataReferenceUpdater */ protected $updated; + /** + * @var bool + */ + protected $shouldSave = true; + /** * Instantiate data reference updater. * @@ -88,132 +94,69 @@ protected function getTopLevelFields() abstract protected function recursivelyUpdateFields($fields, $dottedPrefix = null); /** - * Update nested field values. + * Get fieldtypes that use the UpdatesReferences trait, filtered by cheap string type check. * * @param \Illuminate\Support\Collection $fields - * @param null|string $dottedPrefix - * @return $this - */ - protected function updateNestedFieldValues($fields, $dottedPrefix) - { - $fields - ->filter(function ($field) { - return in_array($field->type(), ['replicator', 'grid', 'group', 'bard']); - }) - ->each(function ($field) use ($dottedPrefix) { - $method = 'update'.ucfirst($field->type()).'Children'; - $dottedKey = $dottedPrefix.$field->handle(); - - $this->{$method}($field, $dottedKey); - }); - - return $this; - } - - /** - * Update replicator field children. - * - * @param \Statamic\Fields\Field $field - * @param string $dottedKey - */ - protected function updateReplicatorChildren($field, $dottedKey) - { - $data = $this->item->data(); - - $sets = Arr::get($data, $dottedKey); - - collect($sets)->each(function ($set, $setKey) use ($dottedKey, $field) { - $dottedPrefix = "{$dottedKey}.{$setKey}."; - $setHandle = Arr::get($set, 'type'); - $fields = Arr::get($field->fieldtype()->flattenedSetsConfig(), "{$setHandle}.fields"); - - if ($setHandle && $fields) { - $this->recursivelyUpdateFields((new Fields($fields))->all(), $dottedPrefix); - } - }); - } - - /** - * Update grid field children. - * - * @param \Statamic\Fields\Field $field - * @param string $dottedKey + * @return \Illuminate\Support\Collection */ - protected function updateGridChildren($field, $dottedKey) + protected function fieldsWithReferenceUpdates($fields) { - $data = $this->item->data(); + $handles = Blink::once('fieldtypes-with-reference-updates', fn () => app('statamic.fieldtypes') + ->filter(fn ($class) => in_array(UpdatesReferences::class, class_uses_recursive($class))) + ->keys()->all() + ); - $sets = Arr::get($data, $dottedKey); - - collect($sets)->each(function ($set, $setKey) use ($dottedKey, $field) { - $dottedPrefix = "{$dottedKey}.{$setKey}."; - $fields = Arr::get($field->config(), 'fields'); - - if ($fields) { - $this->recursivelyUpdateFields((new Fields($fields))->all(), $dottedPrefix); - } - }); + return $fields->filter(fn ($field) => in_array($field->type(), $handles)); } /** - * Update group field children. + * Process nested fields for reference updates at the given dotted prefix. * - * @param \Statamic\Fields\Field $field - * @param string $dottedKey + * @param \Statamic\Fields\Fields $fields + * @param string $dottedPrefix */ - protected function updateGroupChildren($field, $dottedKey) + public function processNestedFields($fields, $dottedPrefix): void { - $data = $this->item->data(); - - $dottedPrefix = "{$dottedKey}."; - $fields = Arr::get($field->config(), 'fields'); - - if ($fields) { - $this->recursivelyUpdateFields((new Fields($fields))->all(), $dottedPrefix); - } + $this->recursivelyUpdateFields($fields->all(), $dottedPrefix); } /** - * Update bard field children. + * Update nested field values by delegating to fieldtype iterateReferenceFields. * - * @param \Statamic\Fields\Field $field - * @param string $dottedKey + * @param \Illuminate\Support\Collection $fields + * @param null|string $dottedPrefix + * @return $this */ - protected function updateBardChildren($field, $dottedKey) + protected function updateNestedFieldValues($fields, $dottedPrefix) { - $data = $this->item->data(); + $this->fieldsWithReferenceUpdates($fields) + ->each(function ($field) use ($dottedPrefix) { + $fieldKey = $dottedPrefix.$field->handle(); + $fieldData = Arr::get($this->item->data()->all(), $fieldKey); - $sets = Arr::get($data, $dottedKey); + if ($fieldData === null) { + return; + } - collect($sets)->each(function ($set, $setKey) use ($dottedKey, $field) { - $dottedPrefix = "{$dottedKey}.{$setKey}.attrs.values."; - $setHandle = Arr::get($set, 'attrs.values.type'); - $fields = Arr::get($field->fieldtype()->flattenedSetsConfig(), "{$setHandle}.fields"); + $field->fieldtype()->iterateReferenceFields( + $fieldData, + new NestedFieldUpdater($this, $fieldKey) + ); + }); - if ($setHandle && $fields) { - $this->recursivelyUpdateFields((new Fields($fields))->all(), $dottedPrefix); - } - }); + return $this; } /** - * Get original value. + * Disable saving after updating references. * - * @return mixed + * @return $this */ - protected function originalValue() + public function withoutSaving() { - return $this->originalValue; - } + $this->shouldSave = false; - /** - * Get new value. - * - * @return mixed - */ - protected function newValue() - { - return $this->newValue; + return $this; } /** @@ -227,101 +170,14 @@ public function isRemovingValue() } /** - * Determine if field has string value. - * - * @param \Statamic\Fields\Field $field - * @param null|string $dottedPrefix - * @return bool - */ - protected function hasStringValue($field, $dottedPrefix) - { - $data = $this->item->data()->all(); - - $dottedKey = $dottedPrefix.$field->handle(); - - return is_string(Arr::get($data, $dottedKey)); - } - - /** - * Update string value on item. - * - * @param \Statamic\Fields\Field $field - * @param null|string $dottedPrefix - */ - protected function updateStringValue($field, $dottedPrefix) - { - $data = $this->item->data()->all(); - - $dottedKey = $dottedPrefix.$field->handle(); - - if (Arr::get($data, $dottedKey) !== $this->originalValue()) { - return; - } - - if ($this->isRemovingValue()) { - Arr::forget($data, $dottedKey); - } else { - Arr::set($data, $dottedKey, $this->newValue()); - } - - $this->item->data($data); - - $this->updated = true; - } - - /** - * Update array value on item. - * - * @param \Statamic\Fields\Field $field - * @param null|string $dottedPrefix + * Save item without triggering individual git commits, as these should be batched into one larger commit. */ - protected function updateArrayValue($field, $dottedPrefix) + protected function saveItem() { - $data = $this->item->data()->all(); - - $dottedKey = $dottedPrefix.$field->handle(); - - $fieldData = Arr::get($data, $dottedKey, []); - - if (! $fieldData) { - return; - } - - $fieldData = collect(Arr::dot($fieldData)); - - if (! $fieldData->contains($this->originalValue())) { + if (! $this->shouldSave) { return; } - $fieldData = $fieldData - ->map(function ($value) { - if ($value === $this->originalValue() && $this->isRemovingValue()) { - return null; - } elseif ($value === $this->originalValue()) { - return $this->newValue(); - } else { - return $value; - } - }) - ->filter() - ->values(); - - if ($fieldData->isEmpty()) { - Arr::forget($data, $dottedKey); - } else { - Arr::set($data, $dottedKey, $fieldData->all()); - } - - $this->item->data($data); - - $this->updated = true; - } - - /** - * Save item without triggering individual git commits, as these should be batched into one larger commit. - */ - protected function saveItem() - { GitSubscriber::withoutListeners(function () { $this->item->save(); }); diff --git a/src/Data/NestedFieldUpdater.php b/src/Data/NestedFieldUpdater.php new file mode 100644 index 00000000000..84ce1813908 --- /dev/null +++ b/src/Data/NestedFieldUpdater.php @@ -0,0 +1,24 @@ +fieldKey.'.'.$dottedPrefix, '.').'.'; + + $this->updater->processNestedFields($fields, $prefix); + } +} diff --git a/src/Fieldtypes/Assets/Assets.php b/src/Fieldtypes/Assets/Assets.php index 6b7e3e00c7e..0004b67739c 100644 --- a/src/Fieldtypes/Assets/Assets.php +++ b/src/Fieldtypes/Assets/Assets.php @@ -16,6 +16,7 @@ use Statamic\Facades\Scope; use Statamic\Facades\User; use Statamic\Fields\Fieldtype; +use Statamic\Fieldtypes\UpdatesReferences; use Statamic\GraphQL\Types\AssetInterface; use Statamic\Http\Resources\CP\Assets\Asset as AssetResource; use Statamic\Query\Scopes\Filter; @@ -24,6 +25,7 @@ class Assets extends Fieldtype { + use UpdatesReferences; protected $categories = ['media', 'relationship']; protected $keywords = ['file', 'files', 'image', 'images', 'video', 'videos', 'audio', 'upload']; protected $selectableInForms = true; @@ -507,4 +509,28 @@ public function toQueryableValue($value) ? collect($value)->first() : collect($value)->filter()->all(); } + + public function replaceAssetReferences($data, ?string $newValue, string $oldValue, string $container) + { + if ($this->configuredContainerHandle() !== $container) { + return $data; + } + + return is_string($data) + ? $this->replaceValue($data, $newValue, $oldValue) + : $this->replaceValuesInArray($data, $newValue, $oldValue); + } + + protected function configuredContainerHandle(): ?string + { + if ($container = $this->config('container')) { + return $container; + } + + $containers = AssetContainer::all(); + + return $containers->count() === 1 + ? $containers->first()->handle() + : null; + } } diff --git a/src/Fieldtypes/Bard.php b/src/Fieldtypes/Bard.php index e3568eefd51..bf43377fda1 100644 --- a/src/Fieldtypes/Bard.php +++ b/src/Fieldtypes/Bard.php @@ -6,6 +6,7 @@ use Facades\Statamic\Fieldtypes\RowId; use Illuminate\Contracts\Validation\DataAwareRule; use Illuminate\Contracts\Validation\ValidationRule; +use Statamic\Data\NestedFieldUpdater; use Statamic\Facades\Asset; use Statamic\Facades\AssetContainer; use Statamic\Facades\Blink; @@ -14,6 +15,7 @@ use Statamic\Facades\GraphQL; use Statamic\Facades\Site; use Statamic\Fields\Field; +use Statamic\Fields\Fields; use Statamic\Fields\Value; use Statamic\Fieldtypes\Bard\Augmentor; use Statamic\GraphQL\Types\BardSetsType; @@ -811,6 +813,59 @@ public static function setDefaultButtons(array $buttons): void static::$defaultButtons = $buttons; } + public function replaceAssetReferences($data, ?string $newValue, string $oldValue, string $container) + { + if ($this->config('container') !== $container) { + return $data; + } + + if (is_string($data)) { + return $data ? $this->replaceStatamicUrls($data, $newValue, $oldValue) : $data; + } + + if (! is_array($data) || empty($data)) { + return $data; + } + + $flat = collect(Arr::dot($data)); + + $flat->filter(fn ($value, $key) => preg_match('/(.*)\.(type)/', $key) && $value === 'image') + ->each(function ($value, $key) use (&$data, $newValue, $oldValue, $container) { + $srcKey = str_replace('.type', '.attrs.src', $key); + + if (Arr::get($data, $srcKey) === "asset::{$container}::{$oldValue}") { + Arr::set($data, $srcKey, $newValue === null ? '' : "asset::{$container}::{$newValue}"); + } + }); + + $flat->filter(fn ($value, $key) => preg_match('/(.*)\.(type)/', $key) && $value === 'link') + ->each(function ($value, $key) use (&$data, $newValue, $oldValue, $container) { + $hrefKey = str_replace('.type', '.attrs.href', $key); + + if (Arr::get($data, $hrefKey) === "statamic://asset::{$container}::{$oldValue}") { + Arr::set($data, $hrefKey, $newValue === null ? '' : "statamic://asset::{$container}::{$newValue}"); + } + }); + + return $data; + } + + public function iterateReferenceFields($data, NestedFieldUpdater $updater): void + { + if (! is_array($data)) { + return; + } + + collect($data)->each(function ($set, $setKey) use ($updater) { + $setHandle = Arr::get($set, 'attrs.values.type'); + $fields = Arr::get($this->flattenedSetsConfig(), "{$setHandle}.fields"); + + if ($setHandle && $fields) { + $updater->update(new Fields($fields), "{$setKey}.attrs.values."); + } + }); + } + private function containerRequiredRule(): ValidationRule { return new class implements DataAwareRule, ValidationRule diff --git a/src/Fieldtypes/Grid.php b/src/Fieldtypes/Grid.php index f396130a2c4..c36478a9ed3 100644 --- a/src/Fieldtypes/Grid.php +++ b/src/Fieldtypes/Grid.php @@ -3,6 +3,7 @@ namespace Statamic\Fieldtypes; use Facades\Statamic\Fieldtypes\RowId; +use Statamic\Data\NestedFieldUpdater; use Statamic\Facades\GraphQL; use Statamic\Fields\Fields; use Statamic\Fields\Fieldtype; @@ -15,6 +16,7 @@ class Grid extends Fieldtype { use AddsEntryValidationReplacements; + use UpdatesReferences; protected $categories = ['structured']; protected $defaultable = false; @@ -277,4 +279,23 @@ public function toQueryableValue($value) { return empty($value) ? null : $value; } + + public function iterateReferenceFields($data, NestedFieldUpdater $updater): void + { + if (! is_array($data)) { + return; + } + + $fields = $this->config('fields'); + + if (! $fields) { + return; + } + + $fields = new Fields($fields); + + foreach (array_keys($data) as $idx) { + $updater->update($fields, "{$idx}."); + } + } } diff --git a/src/Fieldtypes/Group.php b/src/Fieldtypes/Group.php index 28fe7c0f7a1..c8a7bed26f1 100644 --- a/src/Fieldtypes/Group.php +++ b/src/Fieldtypes/Group.php @@ -2,6 +2,7 @@ namespace Statamic\Fieldtypes; +use Statamic\Data\NestedFieldUpdater; use Statamic\Facades\GraphQL; use Statamic\Fields\Fields; use Statamic\Fields\Fieldtype; @@ -12,6 +13,8 @@ class Group extends Fieldtype { + use UpdatesReferences; + protected $categories = ['structured']; protected $defaultable = false; protected $selectableInForms = true; @@ -199,4 +202,15 @@ public function hasJsDriverDataBinding(): bool { return false; } + + public function iterateReferenceFields($data, NestedFieldUpdater $updater): void + { + $fieldsConfig = $this->config('fields'); + + if (! $fieldsConfig) { + return; + } + + $updater->update(new Fields($fieldsConfig)); + } } diff --git a/src/Fieldtypes/Link.php b/src/Fieldtypes/Link.php index 310166ca404..c9c63d7967f 100644 --- a/src/Fieldtypes/Link.php +++ b/src/Fieldtypes/Link.php @@ -17,6 +17,7 @@ class Link extends Fieldtype { + use UpdatesReferences; protected $categories = ['relationship']; protected function configFieldItems(): array @@ -219,4 +220,17 @@ private function availableSites() ->values() ->all(); } + + public function replaceAssetReferences($data, ?string $newValue, string $oldValue, string $container) + { + if ($this->config('container') !== $container) { + return $data; + } + + if ($data !== "asset::{$container}::{$oldValue}") { + return $data; + } + + return $newValue !== null ? "asset::{$container}::{$newValue}" : null; + } } diff --git a/src/Fieldtypes/Markdown.php b/src/Fieldtypes/Markdown.php index e67d197c2a8..9c54b0efb5e 100644 --- a/src/Fieldtypes/Markdown.php +++ b/src/Fieldtypes/Markdown.php @@ -10,11 +10,11 @@ class Markdown extends Fieldtype { + use Concerns\ResolvesStatamicUrls, UpdatesReferences; + protected $categories = ['text']; protected $keywords = ['md', 'content', 'html']; - use Concerns\ResolvesStatamicUrls; - protected function configFieldItems(): array { return [ @@ -251,4 +251,17 @@ public function shouldParseAntlersFromRawString(): bool { return $this->config('smartypants', false); } + + public function replaceAssetReferences($data, ?string $newValue, string $oldValue, string $container) + { + if ($this->config('container') !== $container) { + return $data; + } + + if (! is_string($data) || ! $data) { + return $data; + } + + return $this->replaceStatamicUrls($data, $newValue, $oldValue); + } } diff --git a/src/Fieldtypes/Replicator.php b/src/Fieldtypes/Replicator.php index 04552676ad9..8ff18e06573 100644 --- a/src/Fieldtypes/Replicator.php +++ b/src/Fieldtypes/Replicator.php @@ -3,6 +3,7 @@ namespace Statamic\Fieldtypes; use Facades\Statamic\Fieldtypes\RowId; +use Statamic\Data\NestedFieldUpdater; use Statamic\Facades\Blink; use Statamic\Facades\GraphQL; use Statamic\Fields\Fields; @@ -16,7 +17,7 @@ class Replicator extends Fieldtype { - use AddsEntryValidationReplacements; + use AddsEntryValidationReplacements, UpdatesReferences; protected $categories = ['structured']; protected $keywords = ['builder', 'page builder', 'content']; @@ -319,4 +320,20 @@ public function toQueryableValue($value) { return empty($value) ? null : $value; } + + public function iterateReferenceFields($data, NestedFieldUpdater $updater): void + { + if (! is_array($data)) { + return; + } + + collect($data)->each(function ($set, $setKey) use ($updater) { + $setHandle = Arr::get($set, 'type'); + $fields = Arr::get($this->flattenedSetsConfig(), "{$setHandle}.fields"); + + if ($setHandle && $fields) { + $updater->update(new Fields($fields), "{$setKey}."); + } + }); + } } diff --git a/src/Fieldtypes/Terms.php b/src/Fieldtypes/Terms.php index 1d8f2017afd..6a7142e80e0 100644 --- a/src/Fieldtypes/Terms.php +++ b/src/Fieldtypes/Terms.php @@ -30,6 +30,7 @@ class Terms extends Relationship { + use UpdatesReferences; protected $canEdit = true; protected $canCreate = true; protected $canSearch = true; @@ -530,4 +531,26 @@ public function getItemHint($item): ?string count($this->getConfiguredTaxonomies()) > 1 ? __($item->taxonomy()->title()) : null, ])->filter()->implode(' • '); } + + public function replaceTermReferences($data, ?string $newValue, string $oldValue, string $taxonomy) + { + $configuredTaxonomies = Arr::wrap($this->config('taxonomies')); + + if (count($configuredTaxonomies) > 0) { + if (! in_array($taxonomy, $configuredTaxonomies)) { + return $data; + } + + return is_string($data) + ? $this->replaceValue($data, $newValue, $oldValue) + : $this->replaceValuesInArray($data, $newValue, $oldValue); + } + + $scopedOldValue = "{$taxonomy}::{$oldValue}"; + $scopedNewValue = $newValue !== null ? "{$taxonomy}::{$newValue}" : null; + + return is_string($data) + ? $this->replaceValue($data, $scopedNewValue, $scopedOldValue) + : $this->replaceValuesInArray($data, $scopedNewValue, $scopedOldValue); + } } diff --git a/src/Fieldtypes/UpdatesReferences.php b/src/Fieldtypes/UpdatesReferences.php new file mode 100644 index 00000000000..cd776a995ab --- /dev/null +++ b/src/Fieldtypes/UpdatesReferences.php @@ -0,0 +1,114 @@ +update(Fields $fields, string $relativeDottedPrefix) + */ + public function iterateReferenceFields($data, NestedFieldUpdater $updater): void + { + // Default: no nested fields to process + } + + /** + * Helper: Replace a single value by exact comparison. + * + * @param mixed $data + * @param mixed $newValue + * @param mixed $oldValue + * @return mixed + */ + protected function replaceValue($data, $newValue, $oldValue) + { + return $data === $oldValue ? $newValue : $data; + } + + /** + * Helper: Replace values in a flat or nested array. + * + * @param mixed $data + * @param mixed $newValue + * @param mixed $oldValue + * @return mixed + */ + protected function replaceValuesInArray($data, $newValue, $oldValue) + { + if (! is_array($data) || ! $data) { + return $data; + } + + $flat = collect(Arr::dot($data)); + + if (! $flat->contains($oldValue)) { + return $data; + } + + $result = $flat + ->map(fn ($value) => $value === $oldValue ? $newValue : $value) + ->filter() + ->values(); + + return $result->isEmpty() ? null : $result->all(); + } + + /** + * Helper: Replace statamic:// URLs in a string. + */ + protected function replaceStatamicUrls(string $data, ?string $newValue, string $oldValue): string + { + return preg_replace_callback('/([("])(statamic:\/\/[^()"]*::)([^)"]*)([)"])/im', function ($matches) use ($newValue, $oldValue) { + if ($matches[3] !== $oldValue) { + return $matches[0]; + } + + $replacement = $newValue === null ? '' : $matches[2].$newValue; + + return $matches[1].$replacement.$matches[4]; + }, $data); + } +} diff --git a/src/Listeners/UpdateAssetReferences.php b/src/Listeners/UpdateAssetReferences.php index a7deae9e6fb..1bd79b107ad 100644 --- a/src/Listeners/UpdateAssetReferences.php +++ b/src/Listeners/UpdateAssetReferences.php @@ -97,6 +97,19 @@ protected function replaceReferences($asset, $originalPath, $newPath) if ($updated) { $hasUpdatedItems = true; } + + if (method_exists($item, 'hasWorkingCopy') && $item->hasWorkingCopy()) { + $workingCopyEntry = $item->makeFromRevision($item->workingCopy()); + + $workingCopyUpdated = AssetReferenceUpdater::item($workingCopyEntry) + ->filterByContainer($container) + ->withoutSaving() + ->updateReferences($originalPath, $newPath); + + if ($workingCopyUpdated) { + $workingCopyEntry->saveToWorkingCopy(); + } + } }); if ($hasUpdatedItems) { diff --git a/src/Listeners/UpdateTermReferences.php b/src/Listeners/UpdateTermReferences.php index 308f5f35425..352a1fb15e5 100644 --- a/src/Listeners/UpdateTermReferences.php +++ b/src/Listeners/UpdateTermReferences.php @@ -71,16 +71,34 @@ protected function replaceReferences($term, $originalSlug, $newSlug) $taxonomy = $term->taxonomy()->handle(); - $updatedItems = $this + $hasUpdatedItems = false; + + $this ->getItemsContainingData() - ->map(function ($item) use ($taxonomy, $originalSlug, $newSlug) { - return TermReferenceUpdater::item($item) + ->each(function ($item) use ($taxonomy, $originalSlug, $newSlug, &$hasUpdatedItems) { + $updated = TermReferenceUpdater::item($item) ->filterByTaxonomy($taxonomy) ->updateReferences($originalSlug, $newSlug); - }) - ->filter(); - if ($updatedItems->isNotEmpty()) { + if ($updated) { + $hasUpdatedItems = true; + } + + if (method_exists($item, 'hasWorkingCopy') && $item->hasWorkingCopy()) { + $workingCopyEntry = $item->makeFromRevision($item->workingCopy()); + + $workingCopyUpdated = TermReferenceUpdater::item($workingCopyEntry) + ->filterByTaxonomy($taxonomy) + ->withoutSaving() + ->updateReferences($originalSlug, $newSlug); + + if ($workingCopyUpdated) { + $workingCopyEntry->saveToWorkingCopy(); + } + } + }); + + if ($hasUpdatedItems) { TermReferencesUpdated::dispatch($term); } } diff --git a/src/Revisions/Revisable.php b/src/Revisions/Revisable.php index 4a35e7d3cb8..8091eaa1d73 100644 --- a/src/Revisions/Revisable.php +++ b/src/Revisions/Revisable.php @@ -64,6 +64,18 @@ public function workingCopy() return $revision->toWorkingCopy(); } + public function saveToWorkingCopy() + { + if (! $this->revisionsEnabled() || ! $this->hasWorkingCopy()) { + return false; + } + + $workingCopy = $this->workingCopy(); + $workingCopy->attributes($this->revisionAttributes()); + + return $workingCopy->save(); + } + public function deleteWorkingCopy() { return optional($this->workingCopy())->delete(); diff --git a/src/Taxonomies/TermReferenceUpdater.php b/src/Taxonomies/TermReferenceUpdater.php index 633ccfaf405..97821484e33 100644 --- a/src/Taxonomies/TermReferenceUpdater.php +++ b/src/Taxonomies/TermReferenceUpdater.php @@ -12,11 +12,6 @@ class TermReferenceUpdater extends DataReferenceUpdater */ protected $taxonomy; - /** - * @var null|string - */ - protected $scope; - /** * Filter by taxonomy. * @@ -37,79 +32,37 @@ public function filterByTaxonomy(string $taxonomy) */ protected function recursivelyUpdateFields($fields, $dottedPrefix = null) { - $this - ->updateTermsFieldValues($fields, $dottedPrefix) - ->updateScopedTermsFieldValues($fields, $dottedPrefix) - ->updateNestedFieldValues($fields, $dottedPrefix); - } - - /** - * Update terms field values. - * - * @param \Illuminate\Support\Collection $fields - * @param null|string $dottedPrefix - * @return $this - */ - protected function updateTermsFieldValues($fields, $dottedPrefix) - { - $this->scope = null; - - $fields - ->filter(function ($field) { - return $field->type() === 'terms' - && in_array($this->taxonomy, Arr::wrap($field->get('taxonomies'))); - }) + $this->fieldsWithReferenceUpdates($fields) ->each(function ($field) use ($dottedPrefix) { - $this->hasStringValue($field, $dottedPrefix) - ? $this->updateStringValue($field, $dottedPrefix) - : $this->updateArrayValue($field, $dottedPrefix); - }); + $data = $this->item->data()->all(); + $dottedKey = $dottedPrefix.$field->handle(); + $oldData = Arr::get($data, $dottedKey); - return $this; - } + if ($oldData === null) { + return; + } - /** - * Update scoped terms field values. - * - * @param \Illuminate\Support\Collection $fields - * @param null|string $dottedPrefix - * @return $this - */ - protected function updateScopedTermsFieldValues($fields, $dottedPrefix) - { - $this->scope = "{$this->taxonomy}::"; + $newData = $field->fieldtype()->replaceTermReferences( + $oldData, + $this->newValue, + $this->originalValue, + $this->taxonomy + ); - $fields - ->filter(function ($field) { - return $field->type() === 'terms' - && count(Arr::wrap($field->get('taxonomies'))) === 0; - }) - ->each(function ($field) use ($dottedPrefix) { - $this->hasStringValue($field, $dottedPrefix) - ? $this->updateStringValue($field, $dottedPrefix) - : $this->updateArrayValue($field, $dottedPrefix); - }); + if ($oldData === $newData) { + return; + } - return $this; - } + if ($newData === null && $this->isRemovingValue()) { + Arr::forget($data, $dottedKey); + } else { + Arr::set($data, $dottedKey, $newData); + } - /** - * Get original value. - * - * @return mixed - */ - protected function originalValue() - { - return $this->scope.$this->originalValue; - } + $this->item->data($data); + $this->updated = true; + }); - /** - * Get new value. - * - * @return mixed - */ - protected function newValue() - { - return $this->scope.$this->newValue; + $this->updateNestedFieldValues($fields, $dottedPrefix); } }