From 7c06d89ab07a6340619e3936e6ba293ce733ac0b Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Wed, 29 Apr 2026 15:29:54 +0200 Subject: [PATCH 1/8] add get acf definition (WP-991) Co-Authored-By: Claude Sonnet 4.6 --- .../Extensions/Acf/AcfDynamicSupport.php | 23 ++-- .../Extensions/Acf/AcfTypeDetector.php | 102 +++++++----------- .../ContentRelationsDiscoveryService.php | 5 +- .../ContentRelationsDiscoveryServiceTest.php | 7 +- .../Helpers/DetectChangesHelperTest.php | 3 +- .../Helpers/GutenbergBlockHelperTest.php | 5 +- 6 files changed, 60 insertions(+), 85 deletions(-) diff --git a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php index 7dc5c86b..5607bd79 100644 --- a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php +++ b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php @@ -26,10 +26,6 @@ class AcfDynamicSupport public const POST_TYPE_FIELD = 'acf-field'; public const POST_TYPE_GROUP = 'acf-field-group'; - public const REFERENCED_TYPE_NONE = 'none'; - public const REFERENCED_TYPE_MEDIA = 'media'; - public const REFERENCED_TYPE_POST = 'post'; - public const REFERENCED_TYPE_TAXONOMY = 'taxonomy'; public static array $acfReverseDefinitionAction = []; @@ -200,6 +196,16 @@ private function rawReadGroups(): array return $groups; } + public function getReferencedType(string $type): string + { + return match ($type) { + 'image', 'image_aspect_ratio_crop', 'file', 'gallery' => AcfTypeDetector::REFERENCED_TYPE_MEDIA, + 'post_object', 'page_link', 'relationship' => AcfTypeDetector::REFERENCED_TYPE_POST, + 'taxonomy' => AcfTypeDetector::REFERENCED_TYPE_TAXONOMY, + default => AcfTypeDetector::REFERENCED_TYPE_NONE, + }; + } + private function rawReadFields($parentId): array { return $this->getFieldsFromPosts($this->getQueryByParentId($parentId)->get_posts()); @@ -540,14 +546,7 @@ public function getReferencedTypeByKey($key): string if ($this->definitions === null) { $this->run(); } - $type = $this->definitions[$this->getRuleId($key)]['type'] ?? ''; - - return match ($type) { - 'image', 'image_aspect_ratio_crop', 'file', 'gallery' => self::REFERENCED_TYPE_MEDIA, - 'post_object', 'page_link', 'relationship' => self::REFERENCED_TYPE_POST, - 'taxonomy' => self::REFERENCED_TYPE_TAXONOMY, - default => self::REFERENCED_TYPE_NONE, - }; + return $this->getReferencedType($this->definitions[$this->getRuleId($key)]['type'] ?? ''); } public function removePreTranslationFields(array $data): array diff --git a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php index 7a59156f..21a8a568 100644 --- a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php +++ b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php @@ -13,69 +13,20 @@ class AcfTypeDetector { public const ACF_FIELD_GROUP_REGEX = '#(field|group)_([0-9a-f]){13}#'; - /** - * Default cache time (1 day) - * @var int - */ - public static $cacheExpireSec = 84600; - - /** - * @var Cache - */ - private $cache; + public const REFERENCED_TYPE_POST = 'post'; + public const REFERENCED_TYPE_MEDIA = 'media'; + public const REFERENCED_TYPE_NONE = 'none'; + public const REFERENCED_TYPE_TAXONOMY = 'taxonomy'; - /** - * @var ContentHelper - */ - private $contentHelper; - - /** - * @return Cache - */ - public function getCache() - { - return $this->cache; - } + private const CACHE_EXPIRE_SEC = 84600; - /** - * @param Cache $cache - */ - public function setCache($cache) + private function getCacheKeyByFieldName(string $fieldName): string { - $this->cache = $cache; + return "acf-field-type-cache-$fieldName"; } - /** - * @return ContentHelper - */ - public function getContentHelper() + public function __construct(private AcfDynamicSupport $acfDynamicSupport, private ContentHelper $contentHelper, private Cache $cache) { - return $this->contentHelper; - } - - /** - * @param ContentHelper $contentHelper - */ - public function setContentHelper($contentHelper) - { - $this->contentHelper = $contentHelper; - } - - private function getCacheKeyByFieldName($fieldName) - { - return vsprintf('acf-field-type-cache-%s', [$fieldName]); - } - - /** - * AcfTypeDetector constructor. - * - * @param ContentHelper $contentHelper - * @param Cache $cache - */ - public function __construct(ContentHelper $contentHelper, Cache $cache) - { - $this->setCache($cache); - $this->setContentHelper($contentHelper); } /** @@ -86,8 +37,8 @@ public function __construct(ContentHelper $contentHelper, Cache $cache) */ private function getFieldKeyFieldName($fieldName, SubmissionEntity $submission) { - if (false === $fieldKey = $this->getCache()->get($this->getCacheKeyByFieldName($fieldName))) { - $sourceMeta = $this->getContentHelper()->readSourceMetadata($submission); + if (false === $fieldKey = $this->cache->get($this->getCacheKeyByFieldName($fieldName))) { + $sourceMeta = $this->contentHelper->readSourceMetadata($submission); return $this->getFieldKeyFieldNameByMetaFields($fieldName, $sourceMeta); } return $fieldKey; @@ -95,12 +46,12 @@ private function getFieldKeyFieldName($fieldName, SubmissionEntity $submission) private function getFieldKeyFieldNameByMetaFields($fieldName, array $metadata) { - if (false === $fieldKey = $this->getCache()->get($this->getCacheKeyByFieldName($fieldName))) { + if (false === $fieldKey = $this->cache->get($this->getCacheKeyByFieldName($fieldName))) { $matches = []; $_realFieldName = preg_match('#^(?:meta/)?([^/]+)#i', $fieldName, $matches) ? $matches[1] : $fieldName; if (array_key_exists('_' . $_realFieldName, $metadata)) { $fieldKey = $metadata['_' . $_realFieldName]; - $this->getCache()->set($this->getCacheKeyByFieldName($fieldName), $fieldKey, static::$cacheExpireSec); + $this->cache->set($this->getCacheKeyByFieldName($fieldName), $fieldKey, self::CACHE_EXPIRE_SEC); } else { return false; } @@ -112,17 +63,38 @@ private function getFieldKeyFieldNameByMetaFields($fieldName, array $metadata) public function getProcessorByFieldKey($key, $fieldName) { if (!array_key_exists($key, AcfDynamicSupport::$acfReverseDefinitionAction)) { - MonologWrapper::getLogger(__CLASS__) - ->info(vsprintf('No definition found for field \'%s\', key \'%s\'', [$fieldName, $key])); + $conf = $this->getConfFromAcf($key); + if ($conf === null) { + MonologWrapper::getLogger(__CLASS__) + ->info(vsprintf('No definition found for field \'%s\', key \'%s\'', [$fieldName, $key])); - return false; + return false; + } + } else { + $conf = AcfDynamicSupport::$acfReverseDefinitionAction[$key]; } - $conf = AcfDynamicSupport::$acfReverseDefinitionAction[$key]; $config = array_merge($conf, ['pattern' => vsprintf('^%s$', [$fieldName])]); return CustomFieldFilterHandler::getProcessor(Bootstrap::getContainer(), $config); } + private function getConfFromAcf(string $key): ?array + { + if (!function_exists('acf_get_field')) { + return null; + } + $field = acf_get_field($key); + if (!is_array($field)) { + return null; + } + $type = $this->acfDynamicSupport->getReferencedType($field['type'] ?? ''); + if ($type === self::REFERENCED_TYPE_NONE) { + return null; + } + + return ['action' => 'localize', 'value' => 'reference', 'serialization' => 'none', 'type' => $type]; + } + public function getProcessor($field, SubmissionEntity $submission) { return $this->getAcfProcessor($field, $this->getFieldKeyFieldName($field, $submission)); diff --git a/inc/Smartling/Services/ContentRelationsDiscoveryService.php b/inc/Smartling/Services/ContentRelationsDiscoveryService.php index 3a75f147..6b796be9 100644 --- a/inc/Smartling/Services/ContentRelationsDiscoveryService.php +++ b/inc/Smartling/Services/ContentRelationsDiscoveryService.php @@ -16,6 +16,7 @@ use Smartling\Exception\SmartlingGutenbergParserNotFoundException; use Smartling\Exception\SmartlingHumanReadableException; use Smartling\Extensions\Acf\AcfDynamicSupport; +use Smartling\Extensions\Acf\AcfTypeDetector; use Smartling\Helpers\AbsoluteLinkedAttachmentCoreHelper; use Smartling\Helpers\ArrayHelper; use Smartling\Helpers\ContentHelper; @@ -702,8 +703,8 @@ public function getReferencesFromAcf(GutenbergBlock $block): array } $type = $this->acfDynamicSupport->getReferencedTypeByKey($value); if (in_array($type, [ - AcfDynamicSupport::REFERENCED_TYPE_MEDIA, - AcfDynamicSupport::REFERENCED_TYPE_POST, + AcfTypeDetector::REFERENCED_TYPE_MEDIA, + AcfTypeDetector::REFERENCED_TYPE_POST, ], true)) { $referencedValue = $block->getAttributes()['data'][substr($attribute, 1)] ?? null; if (is_array($referencedValue)) { diff --git a/tests/Services/ContentRelationsDiscoveryServiceTest.php b/tests/Services/ContentRelationsDiscoveryServiceTest.php index 8ea39c0d..7abb1bdf 100644 --- a/tests/Services/ContentRelationsDiscoveryServiceTest.php +++ b/tests/Services/ContentRelationsDiscoveryServiceTest.php @@ -40,6 +40,7 @@ function apply_filters($a, ...$b) { use Smartling\Helpers\FileUriHelper; use Smartling\Helpers\GutenbergBlockHelper; use Smartling\Helpers\GutenbergReplacementRule; + use Smartling\Extensions\Acf\AcfTypeDetector; use Smartling\Helpers\MetaFieldProcessor\BulkProcessors\PostBasedProcessor; use Smartling\Helpers\MetaFieldProcessor\DefaultMetaFieldProcessor; use Smartling\Helpers\MetaFieldProcessor\MetaFieldProcessorManager; @@ -771,11 +772,11 @@ public function testAcfReferencesDetected() { case 1: $this->assertEquals($attachmentKey, $key); - return AcfDynamicSupport::REFERENCED_TYPE_MEDIA; + return AcfTypeDetector::REFERENCED_TYPE_MEDIA; case 2: $this->assertEquals($irrelevantKey, $key); - return AcfDynamicSupport::REFERENCED_TYPE_NONE; + return AcfTypeDetector::REFERENCED_TYPE_NONE; } $this->fail('Expected two calls'); }); @@ -803,7 +804,7 @@ public function testAcfReferencesDetected() { public function testAcfReferencesArraysDetected() { $acfDynamicSupport = $this->createMock(AcfDynamicSupport::class); $acfDynamicSupport->expects($this->once())->method('getReferencedTypeByKey') - ->willReturn(AcfDynamicSupport::REFERENCED_TYPE_POST); + ->willReturn(AcfTypeDetector::REFERENCED_TYPE_POST); $x = $this->getContentRelationDiscoveryService( $this->createMock(ApiWrapper::class), diff --git a/tests/Smartling/Helpers/DetectChangesHelperTest.php b/tests/Smartling/Helpers/DetectChangesHelperTest.php index 7a742f44..ce2648bf 100644 --- a/tests/Smartling/Helpers/DetectChangesHelperTest.php +++ b/tests/Smartling/Helpers/DetectChangesHelperTest.php @@ -6,6 +6,7 @@ use Smartling\DbAl\UploadQueueManager; use Smartling\Exception\SmartlingDbException; use Smartling\Extensions\Acf\AcfDynamicSupport; +use Smartling\Extensions\Acf\AcfTypeDetector; use Smartling\Helpers\ContentSerializationHelper; use Smartling\Helpers\DetectChangesHelper; use Smartling\Settings\ConfigurationProfileEntity; @@ -104,7 +105,7 @@ public function testSubmissionStatusChange() $submissionManager->expects($this->once())->method('storeSubmissions')->with([$submission]); $this->getHelper($settingsManager, $submissionManager) - ->detectChanges(1, 2, AcfDynamicSupport::REFERENCED_TYPE_POST); + ->detectChanges(1, 2, AcfTypeDetector::REFERENCED_TYPE_POST); } public function testAcfFieldGroupNoSubmissionStatusChange() diff --git a/tests/Smartling/Helpers/GutenbergBlockHelperTest.php b/tests/Smartling/Helpers/GutenbergBlockHelperTest.php index ee757117..76f7e8b3 100644 --- a/tests/Smartling/Helpers/GutenbergBlockHelperTest.php +++ b/tests/Smartling/Helpers/GutenbergBlockHelperTest.php @@ -60,6 +60,7 @@ public function get_data() { use PHPUnit\Framework\TestCase; use Smartling\Exception\EntityNotFoundException; use Smartling\Extensions\Acf\AcfDynamicSupport; + use Smartling\Extensions\Acf\AcfTypeDetector; use Smartling\Helpers\ArrayHelper; use Smartling\Helpers\ContentSerializationHelper; use Smartling\Helpers\EventParameters\TranslationStringFilterParameters; @@ -481,9 +482,9 @@ public function testTranslationAttributesWithRelations() ->getMock(); $acfDynamicSupport->method('getReferencedTypeByKey')->willReturnCallback(static function(string $key): string { if ($key === 'field_6006a62721335') { - return AcfDynamicSupport::REFERENCED_TYPE_MEDIA; + return AcfTypeDetector::REFERENCED_TYPE_MEDIA; } - return AcfDynamicSupport::REFERENCED_TYPE_NONE; + return AcfTypeDetector::REFERENCED_TYPE_NONE; }); $acfDynamicSupport->method('validateAcfStores')->willReturn(true); $replacer = $this->createMock(ReplacerInterface::class); From 03bac2c228a6aff97a2c9c72803d4e3f46f41c45 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Wed, 13 May 2026 18:50:34 +0200 Subject: [PATCH 2/8] use acf functions to process fields and groups (WP-991) Co-Authored-By: Claude Sonnet 4.6 --- .../Extensions/Acf/AcfDynamicSupport.php | 160 ++++-------------- .../Extensions/Acf/AcfTypeDetectorTest.php | 3 +- 2 files changed, 38 insertions(+), 125 deletions(-) diff --git a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php index 5607bd79..892a71b3 100644 --- a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php +++ b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php @@ -107,93 +107,55 @@ private function getBlogListForSearch(): array private function getDatabaseDefinitions(): array { $defs = []; - $this->getLogger()->debug('Looking for ACF definitions in the database'); - $blogsToSearch = $this->getBlogListForSearch(); - foreach ($blogsToSearch as $blog) { - $this->getLogger()->debug(vsprintf('Collecting ACF definitions for blog = \'%s\'...', [$blog])); + $this->getLogger()->debug('Looking for ACF definitions via ACF API'); + foreach ($this->getBlogListForSearch() as $blog) { try { - $this->getLogger()->debug(vsprintf('Looking for profiles for blog %s', [$blog])); - $applicableProfiles = $this->settingsManager->findEntityByMainLocale($blog); - if (0 === count($applicableProfiles)) { - $this->getLogger()->debug(vsprintf('No suitable profile found for this blog %s', [$blog])); - } else { - $groups = $this->getGroups($blog); - if (0 < count($groups)) { - foreach ($groups as $groupKey => $group) { - $defs[$groupKey] = [ - 'global_type' => 'group', - 'active' => 1, - ]; - } - foreach ($this->getFields($blog, array_map(static fn($item) => $item['post_id'], $groups)) as $fieldKey => $field) { - $defs[$fieldKey] = [ - 'global_type' => 'field', - 'type' => $field['type'], - ]; - - if ('clone' === $field['type']) { - if (array_key_exists('clone', $field)) { - $defs[$fieldKey]['clone'] = $field['clone']; - } else { - $this->getLogger()->debug('ACF field fieldType="clone" has no target. ' . json_encode($field)); - } - } - } - } + if (0 === count($this->settingsManager->findEntityByMainLocale($blog))) { + $this->getLogger()->debug("No suitable profile found for blog $blog"); + continue; } + $defs = array_merge( + $defs, + $this->siteHelper->withBlog($blog, fn(): array => $this->collectAcfDefinitions()), + ); } catch (\Exception $e) { - $this->getLogger()->warning(vsprintf('ACF Filter generation failed: %s', [$e->getMessage()])); + $this->getLogger()->warning("ACF Filter generation failed: {$e->getMessage()}"); } } return $defs; } - protected function getGroups($blogId): array + private function collectAcfDefinitions(): array { - $dbGroups = []; - $needChange = $this->siteHelper->getCurrentBlogId() !== $blogId; - try { - if ($needChange) { - $this->siteHelper->switchBlogId($blogId); - } - $dbGroups = $this->rawReadGroups(); - } catch (\Exception $e) { - $this->getLogger()->warning( - vsprintf('Error occurred while reading ACF data from blog %s. Message: %s', [$blogId, $e->getMessage()]) - ); - } finally { - if ($needChange) { - $this->siteHelper->restoreBlogId(); - } - } - - return $dbGroups; - } - - /** - * Reads the list of groups from database - */ - private function rawReadGroups(): array - { - $posts = (new \WP_Query( - [ - 'post_type' => self::POST_TYPE_GROUP, - 'suppress_filters' => true, - 'posts_per_page' => -1, - 'post_status' => 'publish', - ] - ))->get_posts(); - - $groups = []; - foreach ($posts as $post) { - $groups[$post->post_name] = [ - 'title' => $post->post_title, - 'post_id' => $post->ID, + $defs = []; + foreach (acf_get_field_groups() as $group) { + $defs[$group['key']] = [ + 'global_type' => 'group', + 'active' => $group['active'] ?? 1, ]; + $stack = [$group]; + while (null !== ($parent = array_shift($stack))) { + foreach (acf_get_fields($parent) as $field) { + if (!is_array($field) || !isset($field['key'], $field['type'])) { + continue; + } + $defs[$field['key']] = ['global_type' => 'field', 'type' => $field['type']]; + if ('clone' === $field['type']) { + if (array_key_exists('clone', $field)) { + $defs[$field['key']]['clone'] = $field['clone']; + } else { + $this->getLogger()->debug('ACF field fieldType="clone" has no target. ' . json_encode($field)); + } + } + if (in_array($field['type'], ['repeater', 'group', 'flexible_content'], true)) { + $stack[] = $field; + } + } + } } - return $groups; + return $defs; } public function getReferencedType(string $type): string @@ -206,18 +168,6 @@ public function getReferencedType(string $type): string }; } - private function rawReadFields($parentId): array - { - return $this->getFieldsFromPosts($this->getQueryByParentId($parentId)->get_posts()); - } - - private function getFields(int $blogId, array $parentIds): array - { - return $this->siteHelper->withBlog($blogId, function () use ($parentIds): array { - return $this->getFieldsFromPosts($this->getQueryByParentIds($parentIds)->get_posts()); - }); - } - protected function extractGroupsDefinitions(array $groups): array { $defs = []; @@ -662,42 +612,4 @@ public function getRuleId(string $key): string return array_pop($matches[0]) ?? $key; } - private function getQueryByParentId($parentId): \WP_Query - { - return new \WP_Query(array_merge($this->getQueryArray(), ['post_parent' => $parentId])); - } - - private function getQueryByParentIds(array $parentIds): \WP_Query - { - return new \WP_Query(array_merge($this->getQueryArray(), ['post_parent__in' => $parentIds])); - } - - private function getQueryArray(): array - { - return [ - 'post_type' => self::POST_TYPE_FIELD, - 'suppress_filters' => true, - 'posts_per_page' => -1, - 'post_status' => 'publish', - ]; - } - - private function getFieldsFromPosts(array $posts): array - { - $fields = []; - foreach ($posts as $post) { - $configuration = unserialize($post->post_content, ['allowed_classes' => false]); - $fieldData = ['type' => $configuration['type']]; - if ($configuration['type'] === 'clone' && array_key_exists('clone', $configuration)) { - $fieldData['clone'] = $configuration['clone']; - } - $fields[$post->post_name] = $fieldData; - $subFields = $this->rawReadFields($post->ID); - if (0 < count($subFields)) { - $fields = array_merge($fields, $subFields); - } - } - - return $fields; - } } diff --git a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php index e20048af..ed9dfe7c 100644 --- a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php +++ b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php @@ -42,6 +42,7 @@ public function testGetProcessorByMetaFields(string $fieldName, array $metaField $cache->method('get')->willReturn(false); $x = $this->getMockBuilder(AcfTypeDetector::class) ->setConstructorArgs([ + $this->createMock(AcfDynamicSupport::class), new ContentHelper( $this->createMock(ContentEntitiesIOFactory::class), $this->createMock(SiteHelper::class), @@ -114,7 +115,7 @@ public function testGetProcessorForGutenberg() '"entity\/post_content\/acf\/testimonial\/data\/_media":"field_5eb1344b55a84"}', true); self::assertInstanceOf( MediaBasedProcessor::class, - (new AcfTypeDetector(new ContentHelper($this->createMock(ContentEntitiesIOFactory::class), $siteHelper, new WordpressFunctionProxyHelper()), new WpObjectCache())) + (new AcfTypeDetector($ads, new ContentHelper($this->createMock(ContentEntitiesIOFactory::class), $siteHelper, new WordpressFunctionProxyHelper()), new WpObjectCache())) ->getProcessorForGutenberg(array_keys($fields)[0], $fields) ); } From ea580489628a5d8c457e74018b55db94956d6e76 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Wed, 13 May 2026 19:01:01 +0200 Subject: [PATCH 3/8] revert dependency changes (WP-991) Co-Authored-By: Claude Sonnet 4.6 --- .../Extensions/Acf/AcfDynamicSupport.php | 13 ++++--- .../Extensions/Acf/AcfTypeDetector.php | 35 +++---------------- .../ContentRelationsDiscoveryService.php | 4 +-- .../ContentRelationsDiscoveryServiceTest.php | 7 ++-- .../Extensions/Acf/AcfTypeDetectorTest.php | 3 +- .../Helpers/DetectChangesHelperTest.php | 3 +- .../Helpers/GutenbergBlockHelperTest.php | 5 ++- 7 files changed, 22 insertions(+), 48 deletions(-) diff --git a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php index 892a71b3..dbb0c8c6 100644 --- a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php +++ b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php @@ -26,6 +26,10 @@ class AcfDynamicSupport public const POST_TYPE_FIELD = 'acf-field'; public const POST_TYPE_GROUP = 'acf-field-group'; + public const REFERENCED_TYPE_NONE = 'none'; + public const REFERENCED_TYPE_MEDIA = 'media'; + public const REFERENCED_TYPE_POST = 'post'; + public const REFERENCED_TYPE_TAXONOMY = 'taxonomy'; public static array $acfReverseDefinitionAction = []; @@ -161,10 +165,10 @@ private function collectAcfDefinitions(): array public function getReferencedType(string $type): string { return match ($type) { - 'image', 'image_aspect_ratio_crop', 'file', 'gallery' => AcfTypeDetector::REFERENCED_TYPE_MEDIA, - 'post_object', 'page_link', 'relationship' => AcfTypeDetector::REFERENCED_TYPE_POST, - 'taxonomy' => AcfTypeDetector::REFERENCED_TYPE_TAXONOMY, - default => AcfTypeDetector::REFERENCED_TYPE_NONE, + 'image', 'image_aspect_ratio_crop', 'file', 'gallery' => self::REFERENCED_TYPE_MEDIA, + 'post_object', 'page_link', 'relationship' => self::REFERENCED_TYPE_POST, + 'taxonomy' => self::REFERENCED_TYPE_TAXONOMY, + default => self::REFERENCED_TYPE_NONE, }; } @@ -611,5 +615,4 @@ public function getRuleId(string $key): string return array_pop($matches[0]) ?? $key; } - } diff --git a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php index 21a8a568..8d339c17 100644 --- a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php +++ b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php @@ -13,10 +13,6 @@ class AcfTypeDetector { public const ACF_FIELD_GROUP_REGEX = '#(field|group)_([0-9a-f]){13}#'; - public const REFERENCED_TYPE_POST = 'post'; - public const REFERENCED_TYPE_MEDIA = 'media'; - public const REFERENCED_TYPE_NONE = 'none'; - public const REFERENCED_TYPE_TAXONOMY = 'taxonomy'; private const CACHE_EXPIRE_SEC = 84600; @@ -25,7 +21,7 @@ private function getCacheKeyByFieldName(string $fieldName): string return "acf-field-type-cache-$fieldName"; } - public function __construct(private AcfDynamicSupport $acfDynamicSupport, private ContentHelper $contentHelper, private Cache $cache) + public function __construct(private ContentHelper $contentHelper, private Cache $cache) { } @@ -63,38 +59,17 @@ private function getFieldKeyFieldNameByMetaFields($fieldName, array $metadata) public function getProcessorByFieldKey($key, $fieldName) { if (!array_key_exists($key, AcfDynamicSupport::$acfReverseDefinitionAction)) { - $conf = $this->getConfFromAcf($key); - if ($conf === null) { - MonologWrapper::getLogger(__CLASS__) - ->info(vsprintf('No definition found for field \'%s\', key \'%s\'', [$fieldName, $key])); + MonologWrapper::getLogger(__CLASS__) + ->info(vsprintf('No definition found for field \'%s\', key \'%s\'', [$fieldName, $key])); - return false; - } - } else { - $conf = AcfDynamicSupport::$acfReverseDefinitionAction[$key]; + return false; } + $conf = AcfDynamicSupport::$acfReverseDefinitionAction[$key]; $config = array_merge($conf, ['pattern' => vsprintf('^%s$', [$fieldName])]); return CustomFieldFilterHandler::getProcessor(Bootstrap::getContainer(), $config); } - private function getConfFromAcf(string $key): ?array - { - if (!function_exists('acf_get_field')) { - return null; - } - $field = acf_get_field($key); - if (!is_array($field)) { - return null; - } - $type = $this->acfDynamicSupport->getReferencedType($field['type'] ?? ''); - if ($type === self::REFERENCED_TYPE_NONE) { - return null; - } - - return ['action' => 'localize', 'value' => 'reference', 'serialization' => 'none', 'type' => $type]; - } - public function getProcessor($field, SubmissionEntity $submission) { return $this->getAcfProcessor($field, $this->getFieldKeyFieldName($field, $submission)); diff --git a/inc/Smartling/Services/ContentRelationsDiscoveryService.php b/inc/Smartling/Services/ContentRelationsDiscoveryService.php index 6b796be9..edd06c7e 100644 --- a/inc/Smartling/Services/ContentRelationsDiscoveryService.php +++ b/inc/Smartling/Services/ContentRelationsDiscoveryService.php @@ -703,8 +703,8 @@ public function getReferencesFromAcf(GutenbergBlock $block): array } $type = $this->acfDynamicSupport->getReferencedTypeByKey($value); if (in_array($type, [ - AcfTypeDetector::REFERENCED_TYPE_MEDIA, - AcfTypeDetector::REFERENCED_TYPE_POST, + AcfDynamicSupport::REFERENCED_TYPE_MEDIA, + AcfDynamicSupport::REFERENCED_TYPE_POST, ], true)) { $referencedValue = $block->getAttributes()['data'][substr($attribute, 1)] ?? null; if (is_array($referencedValue)) { diff --git a/tests/Services/ContentRelationsDiscoveryServiceTest.php b/tests/Services/ContentRelationsDiscoveryServiceTest.php index 7abb1bdf..8ea39c0d 100644 --- a/tests/Services/ContentRelationsDiscoveryServiceTest.php +++ b/tests/Services/ContentRelationsDiscoveryServiceTest.php @@ -40,7 +40,6 @@ function apply_filters($a, ...$b) { use Smartling\Helpers\FileUriHelper; use Smartling\Helpers\GutenbergBlockHelper; use Smartling\Helpers\GutenbergReplacementRule; - use Smartling\Extensions\Acf\AcfTypeDetector; use Smartling\Helpers\MetaFieldProcessor\BulkProcessors\PostBasedProcessor; use Smartling\Helpers\MetaFieldProcessor\DefaultMetaFieldProcessor; use Smartling\Helpers\MetaFieldProcessor\MetaFieldProcessorManager; @@ -772,11 +771,11 @@ public function testAcfReferencesDetected() { case 1: $this->assertEquals($attachmentKey, $key); - return AcfTypeDetector::REFERENCED_TYPE_MEDIA; + return AcfDynamicSupport::REFERENCED_TYPE_MEDIA; case 2: $this->assertEquals($irrelevantKey, $key); - return AcfTypeDetector::REFERENCED_TYPE_NONE; + return AcfDynamicSupport::REFERENCED_TYPE_NONE; } $this->fail('Expected two calls'); }); @@ -804,7 +803,7 @@ public function testAcfReferencesDetected() { public function testAcfReferencesArraysDetected() { $acfDynamicSupport = $this->createMock(AcfDynamicSupport::class); $acfDynamicSupport->expects($this->once())->method('getReferencedTypeByKey') - ->willReturn(AcfTypeDetector::REFERENCED_TYPE_POST); + ->willReturn(AcfDynamicSupport::REFERENCED_TYPE_POST); $x = $this->getContentRelationDiscoveryService( $this->createMock(ApiWrapper::class), diff --git a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php index ed9dfe7c..e20048af 100644 --- a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php +++ b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php @@ -42,7 +42,6 @@ public function testGetProcessorByMetaFields(string $fieldName, array $metaField $cache->method('get')->willReturn(false); $x = $this->getMockBuilder(AcfTypeDetector::class) ->setConstructorArgs([ - $this->createMock(AcfDynamicSupport::class), new ContentHelper( $this->createMock(ContentEntitiesIOFactory::class), $this->createMock(SiteHelper::class), @@ -115,7 +114,7 @@ public function testGetProcessorForGutenberg() '"entity\/post_content\/acf\/testimonial\/data\/_media":"field_5eb1344b55a84"}', true); self::assertInstanceOf( MediaBasedProcessor::class, - (new AcfTypeDetector($ads, new ContentHelper($this->createMock(ContentEntitiesIOFactory::class), $siteHelper, new WordpressFunctionProxyHelper()), new WpObjectCache())) + (new AcfTypeDetector(new ContentHelper($this->createMock(ContentEntitiesIOFactory::class), $siteHelper, new WordpressFunctionProxyHelper()), new WpObjectCache())) ->getProcessorForGutenberg(array_keys($fields)[0], $fields) ); } diff --git a/tests/Smartling/Helpers/DetectChangesHelperTest.php b/tests/Smartling/Helpers/DetectChangesHelperTest.php index ce2648bf..7a742f44 100644 --- a/tests/Smartling/Helpers/DetectChangesHelperTest.php +++ b/tests/Smartling/Helpers/DetectChangesHelperTest.php @@ -6,7 +6,6 @@ use Smartling\DbAl\UploadQueueManager; use Smartling\Exception\SmartlingDbException; use Smartling\Extensions\Acf\AcfDynamicSupport; -use Smartling\Extensions\Acf\AcfTypeDetector; use Smartling\Helpers\ContentSerializationHelper; use Smartling\Helpers\DetectChangesHelper; use Smartling\Settings\ConfigurationProfileEntity; @@ -105,7 +104,7 @@ public function testSubmissionStatusChange() $submissionManager->expects($this->once())->method('storeSubmissions')->with([$submission]); $this->getHelper($settingsManager, $submissionManager) - ->detectChanges(1, 2, AcfTypeDetector::REFERENCED_TYPE_POST); + ->detectChanges(1, 2, AcfDynamicSupport::REFERENCED_TYPE_POST); } public function testAcfFieldGroupNoSubmissionStatusChange() diff --git a/tests/Smartling/Helpers/GutenbergBlockHelperTest.php b/tests/Smartling/Helpers/GutenbergBlockHelperTest.php index 76f7e8b3..ee757117 100644 --- a/tests/Smartling/Helpers/GutenbergBlockHelperTest.php +++ b/tests/Smartling/Helpers/GutenbergBlockHelperTest.php @@ -60,7 +60,6 @@ public function get_data() { use PHPUnit\Framework\TestCase; use Smartling\Exception\EntityNotFoundException; use Smartling\Extensions\Acf\AcfDynamicSupport; - use Smartling\Extensions\Acf\AcfTypeDetector; use Smartling\Helpers\ArrayHelper; use Smartling\Helpers\ContentSerializationHelper; use Smartling\Helpers\EventParameters\TranslationStringFilterParameters; @@ -482,9 +481,9 @@ public function testTranslationAttributesWithRelations() ->getMock(); $acfDynamicSupport->method('getReferencedTypeByKey')->willReturnCallback(static function(string $key): string { if ($key === 'field_6006a62721335') { - return AcfTypeDetector::REFERENCED_TYPE_MEDIA; + return AcfDynamicSupport::REFERENCED_TYPE_MEDIA; } - return AcfTypeDetector::REFERENCED_TYPE_NONE; + return AcfDynamicSupport::REFERENCED_TYPE_NONE; }); $acfDynamicSupport->method('validateAcfStores')->willReturn(true); $replacer = $this->createMock(ReplacerInterface::class); From e571b68ba84ebb6b38868e6750709855d1bf1011 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Thu, 14 May 2026 12:42:56 +0200 Subject: [PATCH 4/8] rework acf fields processing (WP-991) Co-Authored-By: Claude Sonnet 4.6 --- .../Base/SmartlingCoreDownloadTrait.php | 10 +- .../Extensions/Acf/AcfDynamicSupport.php | 234 +++++------------- .../Extensions/Acf/AcfTypeDetector.php | 57 ++--- .../MetaFieldProcessorManager.php | 2 +- .../ContentRelationsDiscoveryService.php | 3 +- inc/config/services.yml | 3 +- .../Extensions/Acf/AcfTypeDetectorTest.php | 12 +- .../Helpers/GutenbergBlockHelperTest.php | 17 +- 8 files changed, 108 insertions(+), 230 deletions(-) diff --git a/inc/Smartling/Base/SmartlingCoreDownloadTrait.php b/inc/Smartling/Base/SmartlingCoreDownloadTrait.php index bb40a4d8..cedf34ef 100644 --- a/inc/Smartling/Base/SmartlingCoreDownloadTrait.php +++ b/inc/Smartling/Base/SmartlingCoreDownloadTrait.php @@ -58,7 +58,7 @@ public function downloadTranslationBySubmission(SubmissionEntity $entity): void $entity->getFileUri(), ]) ); - $data = (string)$this->getApiWrapper()->downloadFile($entity); + $data = $this->getApiWrapper()->downloadFile($entity); $msg = vsprintf('Downloaded file for submission id = \'%s\'. Dump: %s', [$entity->getId(), base64_encode($data)]); $this->getLogger()->debug($msg); @@ -74,9 +74,7 @@ public function downloadTranslationBySubmission(SubmissionEntity $entity): void $entity->getTargetLocale(), ]) ); - if (count($this->acfDynamicSupport->getDefinitions()) === 0) { - $this->acfDynamicSupport->run(); - } + $this->acfDynamicSupport->runIfRequired(); $this->applyXML($entity, $data, $this->xmlHelper, $this->postContentHelper); LiveNotificationController::pushNotification( $this @@ -122,12 +120,12 @@ public function downloadTranslationBySubmission(SubmissionEntity $entity): void } } - public function downloadTranslationBySubmissionId($id) + public function downloadTranslationBySubmissionId($id): void { do_action(ExportedAPI::ACTION_SMARTLING_DOWNLOAD_TRANSLATION, $this->loadSubmissionEntityById($id)); } - public function downloadTranslation($contentType, $sourceBlog, $sourceEntity, $targetBlog, $targetEntity = null) + public function downloadTranslation($contentType, $sourceBlog, $sourceEntity, $targetBlog, $targetEntity = null): void { $submission = $this->getTranslationHelper() ->prepareSubmission($contentType, $sourceBlog, $sourceEntity, $targetBlog, $targetEntity); diff --git a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php index dbb0c8c6..7d6ac1a6 100644 --- a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php +++ b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php @@ -4,7 +4,6 @@ use Smartling\Base\ExportedAPI; use Smartling\Bootstrap; -use Smartling\Exception\SmartlingConfigException; use Smartling\Exception\SmartlingDirectRunRuntimeException; use Smartling\Extensions\AcfOptionPages\ContentTypeAcfOption; use Smartling\Helpers\ArrayHelper; @@ -31,7 +30,7 @@ class AcfDynamicSupport public const REFERENCED_TYPE_POST = 'post'; public const REFERENCED_TYPE_TAXONOMY = 'taxonomy'; - public static array $acfReverseDefinitionAction = []; + private array $filterConfigurations = []; private ?array $definitions = null; @@ -47,20 +46,6 @@ public function getDefinitions(): array return $this->definitions ?? []; } - /** - * @throws SmartlingConfigException - */ - private function getAcf(): mixed - { - global $acf; - - if (!isset($acf)) { - throw new SmartlingConfigException('ACF plugin is not installed or activated.'); - } - - return $acf; - } - public function __construct( private ArrayHelper $arrayHelper, private SettingsManager $settingsManager, @@ -108,7 +93,7 @@ private function getBlogListForSearch(): array /** * @throws SmartlingDirectRunRuntimeException */ - private function getDatabaseDefinitions(): array + private function loadDefinitions(): array { $defs = []; $this->getLogger()->debug('Looking for ACF definitions via ACF API'); @@ -130,177 +115,78 @@ private function getDatabaseDefinitions(): array return $defs; } - private function collectAcfDefinitions(): array + public function getFilterConfiguration(string $key): ?array { - $defs = []; - foreach (acf_get_field_groups() as $group) { - $defs[$group['key']] = [ - 'global_type' => 'group', - 'active' => $group['active'] ?? 1, - ]; - $stack = [$group]; - while (null !== ($parent = array_shift($stack))) { - foreach (acf_get_fields($parent) as $field) { - if (!is_array($field) || !isset($field['key'], $field['type'])) { - continue; - } - $defs[$field['key']] = ['global_type' => 'field', 'type' => $field['type']]; - if ('clone' === $field['type']) { - if (array_key_exists('clone', $field)) { - $defs[$field['key']]['clone'] = $field['clone']; - } else { - $this->getLogger()->debug('ACF field fieldType="clone" has no target. ' . json_encode($field)); - } - } - if (in_array($field['type'], ['repeater', 'group', 'flexible_content'], true)) { - $stack[] = $field; - } - } - } - } - - return $defs; + return $this->filterConfigurations[$key] ?? null; } - public function getReferencedType(string $type): string - { - return match ($type) { - 'image', 'image_aspect_ratio_crop', 'file', 'gallery' => self::REFERENCED_TYPE_MEDIA, - 'post_object', 'page_link', 'relationship' => self::REFERENCED_TYPE_POST, - 'taxonomy' => self::REFERENCED_TYPE_TAXONOMY, - default => self::REFERENCED_TYPE_NONE, - }; - } - - protected function extractGroupsDefinitions(array $groups): array + private function collectAcfDefinitions(): array { $defs = []; - foreach ($groups as $group) { + foreach (acf_get_raw_field_groups() as $group) { + if (!is_array($group) || !isset($group['key'], $group['ID'])) { + continue; + } $defs[$group['key']] = [ 'global_type' => 'group', + 'active' => $group['active'] ?? 1, ]; - if (array_key_exists('active', $group)) { - $defs[$group['key']]['active'] = $group['active']; + foreach (acf_get_raw_fields($group['ID']) as $field) { + $this->addAcfFieldToDefs($field, $defs); } } return $defs; } - protected function extractFieldDefinitions(array $fields): array + private function addAcfFieldToDefs(mixed $field, array &$defs): void { - $defs = []; - - foreach ($fields as $field) { - $defs[$field['key']] = [ - 'global_type' => 'field', - 'type' => $field['type'], - 'name' => $field['name'], - 'parent' => $field['parent'], - ]; - - if ('clone' === $field['type']) { + if (!is_array($field) || !isset($field['key'], $field['type'])) { + return; + } + $defs[$field['key']] = ['global_type' => 'field', 'type' => $field['type']]; + if ('clone' === $field['type']) { + if (array_key_exists('clone', $field)) { $defs[$field['key']]['clone'] = $field['clone']; + } else { + $this->getLogger()->debug('ACF field fieldType="clone" has no target. ' . json_encode($field)); } } - - return $defs; - } - - /** - * Get local definitions for ACF Pro ver < 5.7.12 - */ - private function getLocalDefinitionsOld(): array - { - $defs = []; - try { - $acf = (array)$this->getAcf(); - } catch (SmartlingConfigException $e) { - $this->getLogger()->warning($e->getMessage()); - $this->getLogger()->warning('Unable to load old type local ACF definitions.'); - - return $defs; + if (!in_array($field['type'], ['repeater', 'group', 'flexible_content'], true)) { + return; } - - if (array_key_exists('local', $acf)) { - if ($acf['local'] instanceof \acf_local) { - $local = $acf['local']; - - $defs = array_merge($defs, $this->extractGroupsDefinitions($local->groups)); - $defs = array_merge($defs, $this->extractFieldDefinitions($local->fields)); - + if (isset($field['ID']) && (int)$field['ID'] > 0) { + foreach (acf_get_raw_fields((int)$field['ID']) as $child) { + $this->addAcfFieldToDefs($child, $defs); } } - - return $defs; - } - - protected function validateAcfStores(): bool - { - global $acf_stores; - - return is_array($acf_stores) - && array_key_exists('local-groups', $acf_stores) - && ($acf_stores['local-groups'] instanceof \ACF_Data) - && array_key_exists('local-fields', $acf_stores) - && ($acf_stores['local-fields'] instanceof \ACF_Data); - } - - /** - * Get local definitions for ACF Pro ver 5.7.12+ - */ - private function getLocalDefinitionsNew(): array - { - $defs = []; - - if ($this->validateAcfStores()) { - global $acf_stores; - - $defs = array_merge($defs, $this->extractGroupsDefinitions($acf_stores['local-groups']->get_data())); - $defs = array_merge($defs, $this->extractFieldDefinitions($acf_stores['local-fields']->get_data())); - - } else { - $this->getLogger()->warning('Unable to load new type local ACF definitions.'); - } - - return $defs; - } - - /** - * Reads local (PHP and JSON) ACF Definitions - */ - private function getLocalDefinitions(): array - { - $defs = $this->getLocalDefinitionsOld(); - - if (empty($defs)) { - $defs = $this->getLocalDefinitionsNew(); - } - - return $defs; - } - - private function verifyDefinitions(array $localDefinitions, array $dbDefinitions): bool - { - foreach ($dbDefinitions as $key => $definition) { - if (!array_key_exists($key, $localDefinitions)) { - return false; + if (isset($field['sub_fields']) && is_array($field['sub_fields'])) { + foreach ($field['sub_fields'] as $child) { + $this->addAcfFieldToDefs($child, $defs); } - - if ($definition['global_type'] === 'field') { - $local = $localDefinitions[$key]; - if ($local['type'] !== $definition['type']) { - // ACF Option Pages has internal issue in definition, so skip it: - if ('group_572b269b668a4' !== $local['parent']) { - return false; + } + if (isset($field['layouts']) && is_array($field['layouts'])) { + foreach ($field['layouts'] as $layout) { + if (is_array($layout) && isset($layout['sub_fields']) && is_array($layout['sub_fields'])) { + foreach ($layout['sub_fields'] as $child) { + $this->addAcfFieldToDefs($child, $defs); } } } } + } - return true; + public function getReferencedType(string $type): string + { + return match ($type) { + 'image', 'image_aspect_ratio_crop', 'file', 'gallery' => self::REFERENCED_TYPE_MEDIA, + 'post_object', 'page_link', 'relationship' => self::REFERENCED_TYPE_POST, + 'taxonomy' => self::REFERENCED_TYPE_TAXONOMY, + default => self::REFERENCED_TYPE_NONE, + }; } + private function tryRegisterACFOptions(): void { $this->getLogger()->debug('Checking if ACF Option Pages presents...'); @@ -399,31 +285,16 @@ private function tryRegisterACF(): void $this->getLogger()->debug('Checking if ACF presents...'); if ($this->isAcfActive()) { $this->getLogger()->debug('ACF detected.'); - $localDefinitions = $this->getLocalDefinitions(); - try { - $dbDefinitions = $this->getDatabaseDefinitions(); + $this->definitions = $this->loadDefinitions(); } catch (SmartlingDirectRunRuntimeException $e) { - $dbDefinitions = []; + $this->definitions = []; DiagnosticsHelper::addDiagnosticsMessage( - 'Failed to get ACF definitions from database.' . + 'Failed to get ACF definitions. ' . 'Please ensure that WordPress network is set up properly.
' . "Exception message: {$e->getMessage()}" ); } - - if (false === $this->verifyDefinitions($localDefinitions, $dbDefinitions)) { - $url = admin_url('edit.php?post_type=acf-field-group&page=acf-tools'); - $msg = [ - 'ACF Configuration has been changed.', - 'Please update groups and fields definitions for all sites (As PHP generated code).', - vsprintf('Use this page to generate export code and add it to your theme or extra plugin.', - [$url]), - ]; - DiagnosticsHelper::addDiagnosticsMessage(implode('
', $msg)); - } - - $this->definitions = array_merge($localDefinitions, $dbDefinitions); $this->buildRules(); $this->prepareFilters(); } else { @@ -437,6 +308,13 @@ public function run(): void $this->tryRegisterACF(); } + public function runIfRequired(): void + { + if (count($this->getDefinitions()) === 0) { + $this->run(); + } + } + private function prepareFilters(): void { $rules = []; @@ -468,7 +346,7 @@ private function prepareFilters(): void } } - static::$acfReverseDefinitionAction = $rules; + $this->filterConfigurations = $rules; } public function getReplacerIdForField(array $attributes, string $key): ?string diff --git a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php index 8d339c17..1d88a286 100644 --- a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php +++ b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php @@ -5,13 +5,14 @@ use Smartling\Bootstrap; use Smartling\Helpers\Cache; use Smartling\Helpers\ContentHelper; +use Smartling\Helpers\LoggerSafeTrait; use Smartling\Helpers\MetaFieldProcessor\CustomFieldFilterHandler; use Smartling\Helpers\MetaFieldProcessor\MetaFieldProcessorInterface; -use Smartling\MonologWrapper\MonologWrapper; use Smartling\Submissions\SubmissionEntity; class AcfTypeDetector { + use LoggerSafeTrait; public const ACF_FIELD_GROUP_REGEX = '#(field|group)_([0-9a-f]){13}#'; private const CACHE_EXPIRE_SEC = 84600; @@ -21,17 +22,15 @@ private function getCacheKeyByFieldName(string $fieldName): string return "acf-field-type-cache-$fieldName"; } - public function __construct(private ContentHelper $contentHelper, private Cache $cache) + public function __construct( + private AcfDynamicSupport $acfDynamicSupport, + private Cache $cache, + private ContentHelper $contentHelper, + ) { } - /** - * @param string $fieldName - * @param SubmissionEntity $submission - * - * @return false|string - */ - private function getFieldKeyFieldName($fieldName, SubmissionEntity $submission) + private function getFieldKeyFieldName(string $fieldName, SubmissionEntity $submission): ?string { if (false === $fieldKey = $this->cache->get($this->getCacheKeyByFieldName($fieldName))) { $sourceMeta = $this->contentHelper->readSourceMetadata($submission); @@ -40,7 +39,7 @@ private function getFieldKeyFieldName($fieldName, SubmissionEntity $submission) return $fieldKey; } - private function getFieldKeyFieldNameByMetaFields($fieldName, array $metadata) + private function getFieldKeyFieldNameByMetaFields(string $fieldName, array $metadata): ?string { if (false === $fieldKey = $this->cache->get($this->getCacheKeyByFieldName($fieldName))) { $matches = []; @@ -49,47 +48,43 @@ private function getFieldKeyFieldNameByMetaFields($fieldName, array $metadata) $fieldKey = $metadata['_' . $_realFieldName]; $this->cache->set($this->getCacheKeyByFieldName($fieldName), $fieldKey, self::CACHE_EXPIRE_SEC); } else { - return false; + return null; } } return $fieldKey; } - public function getProcessorByFieldKey($key, $fieldName) + public function getProcessorByFieldKey(string $key, string $fieldName): ?MetaFieldProcessorInterface { - if (!array_key_exists($key, AcfDynamicSupport::$acfReverseDefinitionAction)) { - MonologWrapper::getLogger(__CLASS__) - ->info(vsprintf('No definition found for field \'%s\', key \'%s\'', [$fieldName, $key])); + $this->acfDynamicSupport->runIfRequired(); + $configuration = $this->acfDynamicSupport->getFilterConfiguration($key); + if ($configuration === null) { + $this->getLogger()->info(sprintf('No definition found for fieldName="%s", key="%s"', $fieldName, $key)); - return false; + return null; } - $conf = AcfDynamicSupport::$acfReverseDefinitionAction[$key]; - $config = array_merge($conf, ['pattern' => vsprintf('^%s$', [$fieldName])]); - return CustomFieldFilterHandler::getProcessor(Bootstrap::getContainer(), $config); + $configuration['pattern'] = sprintf('^%s$', $fieldName); + $result = CustomFieldFilterHandler::getProcessor(Bootstrap::getContainer(), $configuration); + return $result ?: null; } - public function getProcessor($field, SubmissionEntity $submission) + public function getProcessor(string $field, SubmissionEntity $submission): ?MetaFieldProcessorInterface { return $this->getAcfProcessor($field, $this->getFieldKeyFieldName($field, $submission)); } - public function getProcessorByMetaFields($field, array $metaFields) + public function getProcessorByMetaFields($field, array $metaFields): ?MetaFieldProcessorInterface { return $this->getAcfProcessor($field, $this->getFieldKeyFieldNameByMetaFields($field, $metaFields)); } - /** - * @param string $field - * @param array $fields - * @return bool|MetaFieldProcessorInterface - */ - public function getProcessorForGutenberg($field, array $fields) + public function getProcessorForGutenberg(string $field, array $fields): ?MetaFieldProcessorInterface { $parts = explode('/', $field); $lastPart = end($parts); - if ($lastPart !== false && strpos($lastPart, '_') !== 0) { + if (!str_starts_with($lastPart, '_')) { $parts[count($parts) - 1] = "_$lastPart"; $acfField = implode('/', $parts); if (array_key_exists($acfField, $fields) && is_string($fields[$acfField])) { @@ -97,10 +92,10 @@ public function getProcessorForGutenberg($field, array $fields) } } - return false; + return null; } - private function getAcfProcessor($field, $key) + private function getAcfProcessor($field, $key): ?MetaFieldProcessorInterface { $matches = []; preg_match_all(self::ACF_FIELD_GROUP_REGEX, $key, $matches); @@ -109,6 +104,6 @@ private function getAcfProcessor($field, $key) return $this->getProcessorByFieldKey($fieldKey, $field); } - return false; + return null; } } diff --git a/inc/Smartling/Helpers/MetaFieldProcessor/MetaFieldProcessorManager.php b/inc/Smartling/Helpers/MetaFieldProcessor/MetaFieldProcessorManager.php index fc0ee1de..6a538e48 100644 --- a/inc/Smartling/Helpers/MetaFieldProcessor/MetaFieldProcessorManager.php +++ b/inc/Smartling/Helpers/MetaFieldProcessor/MetaFieldProcessorManager.php @@ -101,7 +101,7 @@ private function tryGetAcfProcessor($fieldName, SubmissionEntity $submission, Me { if (in_array(get_class($processor), [DefaultMetaFieldProcessor::class, PostContentProcessor::class], true)) { $_processor = $this->getAcfTypeDetector()->getProcessor($fieldName, $submission); - if (false !== $_processor) { + if ($_processor !== null) { return $_processor; } } diff --git a/inc/Smartling/Services/ContentRelationsDiscoveryService.php b/inc/Smartling/Services/ContentRelationsDiscoveryService.php index edd06c7e..108c2dec 100644 --- a/inc/Smartling/Services/ContentRelationsDiscoveryService.php +++ b/inc/Smartling/Services/ContentRelationsDiscoveryService.php @@ -16,7 +16,6 @@ use Smartling\Exception\SmartlingGutenbergParserNotFoundException; use Smartling\Exception\SmartlingHumanReadableException; use Smartling\Extensions\Acf\AcfDynamicSupport; -use Smartling\Extensions\Acf\AcfTypeDetector; use Smartling\Helpers\AbsoluteLinkedAttachmentCoreHelper; use Smartling\Helpers\ArrayHelper; use Smartling\Helpers\ContentHelper; @@ -519,7 +518,7 @@ public function getRelations(string $contentType, int $id, array $targetBlogIds) $this->getLogger()->debug(vsprintf('Trying to treat \'%s\' field as ACF', [$fName])); $acfTypeDetector = $this->metaFieldProcessorManager->getAcfTypeDetector(); $processor = $acfTypeDetector->getProcessorByMetaFields($fName, $content['meta']); - if ($processor === false) { + if ($processor === null) { $processor = $acfTypeDetector->getProcessorForGutenberg($fName, $fields); } } diff --git a/inc/config/services.yml b/inc/config/services.yml index 34d701f1..d0243488 100644 --- a/inc/config/services.yml +++ b/inc/config/services.yml @@ -609,8 +609,9 @@ services: acf.type.detector: class: Smartling\Extensions\Acf\AcfTypeDetector arguments: - - "@content.helper" + - "@acf.dynamic.support" - "@site.cache" + - "@content.helper" wp.translation.lock: class: Smartling\WP\Controller\TranslationLockController diff --git a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php index e20048af..6f9633a2 100644 --- a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php +++ b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php @@ -42,6 +42,7 @@ public function testGetProcessorByMetaFields(string $fieldName, array $metaField $cache->method('get')->willReturn(false); $x = $this->getMockBuilder(AcfTypeDetector::class) ->setConstructorArgs([ + $this->createMock(AcfDynamicSupport::class), new ContentHelper( $this->createMock(ContentEntitiesIOFactory::class), $this->createMock(SiteHelper::class), @@ -114,8 +115,15 @@ public function testGetProcessorForGutenberg() '"entity\/post_content\/acf\/testimonial\/data\/_media":"field_5eb1344b55a84"}', true); self::assertInstanceOf( MediaBasedProcessor::class, - (new AcfTypeDetector(new ContentHelper($this->createMock(ContentEntitiesIOFactory::class), $siteHelper, new WordpressFunctionProxyHelper()), new WpObjectCache())) - ->getProcessorForGutenberg(array_keys($fields)[0], $fields) + (new AcfTypeDetector( + $ads, + new WpObjectCache(), + new ContentHelper( + $this->createMock(ContentEntitiesIOFactory::class), + $siteHelper, + new WordpressFunctionProxyHelper(), + ) + ))->getProcessorForGutenberg(array_keys($fields)[0], $fields) ); } } diff --git a/tests/Smartling/Helpers/GutenbergBlockHelperTest.php b/tests/Smartling/Helpers/GutenbergBlockHelperTest.php index ee757117..0d5db5bc 100644 --- a/tests/Smartling/Helpers/GutenbergBlockHelperTest.php +++ b/tests/Smartling/Helpers/GutenbergBlockHelperTest.php @@ -454,13 +454,6 @@ public function processTranslationAttributesDataSource(): array public function testTranslationAttributesWithRelations() { - global $acf_stores; // validated in AcfDynamicSupport - $acf_stores = [ - 'local-fields' => new ACFish_Data( - [['key' => 'field_6006a62721335', 'type' => 'image', 'name' => '', 'parent' => '']], - ), - 'local-groups' => new ACFish_Data(), - ]; $wpProxy = $this->createMock(WordpressFunctionProxyHelper::class); $wpProxy->method('get_post_types')->willReturn([ 'acf-field' => 'acf-field', @@ -476,7 +469,7 @@ public function testTranslationAttributesWithRelations() ]) ->onlyMethods([ 'getReferencedTypeByKey', - 'validateAcfStores', + 'getReplacerIdForField', ]) ->getMock(); $acfDynamicSupport->method('getReferencedTypeByKey')->willReturnCallback(static function(string $key): string { @@ -485,7 +478,13 @@ public function testTranslationAttributesWithRelations() } return AcfDynamicSupport::REFERENCED_TYPE_NONE; }); - $acfDynamicSupport->method('validateAcfStores')->willReturn(true); + $acfDynamicSupport->method('getReplacerIdForField')->willReturnCallback(static function(array $attributes, string $key): ?string { + $pointer = preg_replace('#(^|/)([^/]+)$#', '$1_$2', $key); + if (($attributes[$pointer] ?? null) === 'field_6006a62721335') { + return ReplacerFactory::REPLACER_RELATED; + } + return null; + }); $replacer = $this->createMock(ReplacerInterface::class); $replacer->expects($this->exactly(2))->method('processAttributeOnDownload')->willReturnCallback(static function($originalValue) { $replacements = [ From 7949ea01574c75d9d7af7df779de9ea121e8ef12 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Thu, 14 May 2026 16:20:44 +0200 Subject: [PATCH 5/8] review changes (WP-991) add max recursion depth, stricter types, improve run if needed Co-Authored-By: Claude Sonnet 4.6 --- .../Extensions/Acf/AcfDynamicSupport.php | 25 +++++++++++++------ .../Extensions/Acf/AcfTypeDetector.php | 2 +- .../Extensions/Acf/AcfTypeDetectorTest.php | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php index 7d6ac1a6..dfbfbd71 100644 --- a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php +++ b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php @@ -139,9 +139,19 @@ private function collectAcfDefinitions(): array return $defs; } - private function addAcfFieldToDefs(mixed $field, array &$defs): void + private const MAX_ACF_FIELD_DEPTH = 16; + + private function addAcfFieldToDefs(array $field, array &$defs, int $depth = 0): void { - if (!is_array($field) || !isset($field['key'], $field['type'])) { + if (!isset($field['key'], $field['type'])) { + return; + } + if ($depth > self::MAX_ACF_FIELD_DEPTH) { + $this->getLogger()->error(sprintf( + 'ACF field tree exceeded depth limit %d at field key "%s"; aborting recursion.', + self::MAX_ACF_FIELD_DEPTH, + $field['key'], + )); return; } $defs[$field['key']] = ['global_type' => 'field', 'type' => $field['type']]; @@ -157,19 +167,19 @@ private function addAcfFieldToDefs(mixed $field, array &$defs): void } if (isset($field['ID']) && (int)$field['ID'] > 0) { foreach (acf_get_raw_fields((int)$field['ID']) as $child) { - $this->addAcfFieldToDefs($child, $defs); + $this->addAcfFieldToDefs($child, $defs, $depth + 1); } } if (isset($field['sub_fields']) && is_array($field['sub_fields'])) { foreach ($field['sub_fields'] as $child) { - $this->addAcfFieldToDefs($child, $defs); + $this->addAcfFieldToDefs($child, $defs, $depth + 1); } } if (isset($field['layouts']) && is_array($field['layouts'])) { foreach ($field['layouts'] as $layout) { if (is_array($layout) && isset($layout['sub_fields']) && is_array($layout['sub_fields'])) { foreach ($layout['sub_fields'] as $child) { - $this->addAcfFieldToDefs($child, $defs); + $this->addAcfFieldToDefs($child, $defs, $depth + 1); } } } @@ -186,7 +196,6 @@ public function getReferencedType(string $type): string }; } - private function tryRegisterACFOptions(): void { $this->getLogger()->debug('Checking if ACF Option Pages presents...'); @@ -310,7 +319,7 @@ public function run(): void public function runIfRequired(): void { - if (count($this->getDefinitions()) === 0) { + if ($this->definitions === null) { $this->run(); } } @@ -373,7 +382,7 @@ public function getReplacerIdForField(array $attributes, string $key): ?string return null; } - public function getReferencedTypeByKey($key): string + public function getReferencedTypeByKey(string $key): string { if ($this->definitions === null) { $this->run(); diff --git a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php index 1d88a286..59453bab 100644 --- a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php +++ b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php @@ -15,7 +15,7 @@ class AcfTypeDetector use LoggerSafeTrait; public const ACF_FIELD_GROUP_REGEX = '#(field|group)_([0-9a-f]){13}#'; - private const CACHE_EXPIRE_SEC = 84600; + private const CACHE_EXPIRE_SEC = 86400; private function getCacheKeyByFieldName(string $fieldName): string { diff --git a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php index 6f9633a2..9a16e71d 100644 --- a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php +++ b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php @@ -43,12 +43,12 @@ public function testGetProcessorByMetaFields(string $fieldName, array $metaField $x = $this->getMockBuilder(AcfTypeDetector::class) ->setConstructorArgs([ $this->createMock(AcfDynamicSupport::class), + $cache, new ContentHelper( $this->createMock(ContentEntitiesIOFactory::class), $this->createMock(SiteHelper::class), new WordpressFunctionProxyHelper() ), - $cache, ]) ->onlyMethods(["getProcessorByFieldKey"]) ->getMock(); From fea464ea3ae7ac259e3f17883057aae1d1653034 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Thu, 14 May 2026 16:23:53 +0200 Subject: [PATCH 6/8] use public acf api (WP-991) Co-Authored-By: Claude Sonnet 4.6 --- inc/Smartling/Extensions/Acf/AcfDynamicSupport.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php index dfbfbd71..21cc7505 100644 --- a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php +++ b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php @@ -123,15 +123,21 @@ public function getFilterConfiguration(string $key): ?array private function collectAcfDefinitions(): array { $defs = []; - foreach (acf_get_raw_field_groups() as $group) { - if (!is_array($group) || !isset($group['key'], $group['ID'])) { + foreach (acf_get_field_groups() as $group) { + if (!is_array($group) || !isset($group['key'])) { continue; } $defs[$group['key']] = [ 'global_type' => 'group', 'active' => $group['active'] ?? 1, ]; - foreach (acf_get_raw_fields($group['ID']) as $field) { + $groupId = (int)($group['ID'] ?? 0); + if ($groupId <= 0) { + // Local-only field group (no DB post) — children live only in the local store, + // which we intentionally don't read from. Skip child enumeration for this group. + continue; + } + foreach (acf_get_raw_fields($groupId) as $field) { $this->addAcfFieldToDefs($field, $defs); } } From a404423232d439c6660bc8d126f8a4a3b3e5bc80 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Thu, 14 May 2026 16:38:29 +0200 Subject: [PATCH 7/8] add test (WP-991) Co-Authored-By: Claude Sonnet 4.6 --- .../Extensions/Acf/AcfDynamicSupport.php | 6 +- .../Extensions/Acf/AcfTypeDetector.php | 2 +- .../Extensions/Acf/AcfDynamicSupportTest.php | 82 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php index 21cc7505..57dd9e04 100644 --- a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php +++ b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php @@ -137,7 +137,7 @@ private function collectAcfDefinitions(): array // which we intentionally don't read from. Skip child enumeration for this group. continue; } - foreach (acf_get_raw_fields($groupId) as $field) { + foreach (acf_get_fields($group) as $field) { $this->addAcfFieldToDefs($field, $defs); } } @@ -147,7 +147,7 @@ private function collectAcfDefinitions(): array private const MAX_ACF_FIELD_DEPTH = 16; - private function addAcfFieldToDefs(array $field, array &$defs, int $depth = 0): void + protected function addAcfFieldToDefs(array $field, array &$defs, int $depth = 0): void { if (!isset($field['key'], $field['type'])) { return; @@ -172,7 +172,7 @@ private function addAcfFieldToDefs(array $field, array &$defs, int $depth = 0): return; } if (isset($field['ID']) && (int)$field['ID'] > 0) { - foreach (acf_get_raw_fields((int)$field['ID']) as $child) { + foreach (acf_get_fields($field) as $child) { $this->addAcfFieldToDefs($child, $defs, $depth + 1); } } diff --git a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php index 59453bab..31f4acd8 100644 --- a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php +++ b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php @@ -95,7 +95,7 @@ public function getProcessorForGutenberg(string $field, array $fields): ?MetaFie return null; } - private function getAcfProcessor($field, $key): ?MetaFieldProcessorInterface + private function getAcfProcessor(string $field, ?string $key): ?MetaFieldProcessorInterface { $matches = []; preg_match_all(self::ACF_FIELD_GROUP_REGEX, $key, $matches); diff --git a/tests/Smartling/Extensions/Acf/AcfDynamicSupportTest.php b/tests/Smartling/Extensions/Acf/AcfDynamicSupportTest.php index dc02b318..64113e89 100644 --- a/tests/Smartling/Extensions/Acf/AcfDynamicSupportTest.php +++ b/tests/Smartling/Extensions/Acf/AcfDynamicSupportTest.php @@ -149,4 +149,86 @@ public function testGetRuleId() $this->assertEquals('field_66d0680a343ff', $x->getRuleId('field_66d0680a343ff'), 'Should return rule id'); $this->assertEquals('field_66d08bd321aee', $x->getRuleId('field_66d0680a343ff_field_66d08bd321aee'), 'Should return last part of complex rule id'); } + + public function testAddAcfFieldToDefsRecursesInlineSubFieldsAndLayouts() + { + $x = $this->getAcfDynamicSupportWithExposedAddField(); + + // Repeater carrying its sub_fields inline (Bluebeam-style import where children + // are not stored as separate acf-field posts). ID is 0 so the DB recursion is + // skipped and we exercise only the inline path. + $field = [ + 'key' => 'field_repeater', + 'type' => 'repeater', + 'ID' => 0, + 'sub_fields' => [ + ['key' => 'field_inner_repeater', 'type' => 'repeater', 'ID' => 0, 'sub_fields' => [ + ['key' => 'field_image_leaf', 'type' => 'image'], + ['key' => 'field_clone_leaf', 'type' => 'clone', 'clone' => ['field_target']], + ]], + ['key' => 'field_flex', 'type' => 'flexible_content', 'ID' => 0, 'layouts' => [ + ['key' => 'layout_a', 'sub_fields' => [ + ['key' => 'field_layout_file', 'type' => 'file'], + ]], + ]], + ], + ]; + $defs = []; + $x->callAddAcfFieldToDefs($field, $defs); + + $this->assertSame(['global_type' => 'field', 'type' => 'repeater'], $defs['field_repeater']); + $this->assertSame(['global_type' => 'field', 'type' => 'repeater'], $defs['field_inner_repeater']); + $this->assertSame(['global_type' => 'field', 'type' => 'image'], $defs['field_image_leaf']); + $this->assertSame( + ['global_type' => 'field', 'type' => 'clone', 'clone' => ['field_target']], + $defs['field_clone_leaf'], + 'clone field should preserve its clone target list', + ); + $this->assertSame(['global_type' => 'field', 'type' => 'flexible_content'], $defs['field_flex']); + $this->assertSame( + ['global_type' => 'field', 'type' => 'file'], + $defs['field_layout_file'], + 'sub_fields nested in a flexible_content layout should be picked up', + ); + } + + public function testAddAcfFieldToDefsHonoursDepthGuard() + { + $x = $this->getAcfDynamicSupportWithExposedAddField(); + + // Build a pathologically deep repeater chain (20 levels — exceeds MAX_ACF_FIELD_DEPTH=16). + // The deepest leaf must NOT make it into defs. + $leafKey = 'field_too_deep_leaf'; + $field = ['key' => $leafKey, 'type' => 'image']; + for ($i = 19; $i >= 0; $i--) { + $field = [ + 'key' => "field_level_$i", + 'type' => 'repeater', + 'ID' => 0, + 'sub_fields' => [$field], + ]; + } + $defs = []; + $x->callAddAcfFieldToDefs($field, $defs); + + $this->assertArrayHasKey('field_level_0', $defs, 'top-level container should be in defs'); + $this->assertArrayHasKey('field_level_16', $defs, 'fields up to MAX depth should be in defs'); + $this->assertArrayNotHasKey($leafKey, $defs, 'leaf beyond depth limit must be dropped'); + } + + private function getAcfDynamicSupportWithExposedAddField(): AcfDynamicSupport + { + return new class( + new ArrayHelper(), + $this->createMock(SettingsManager::class), + $this->createMock(SiteHelper::class), + $this->createMock(SubmissionManager::class), + $this->createMock(WordpressFunctionProxyHelper::class), + ) extends AcfDynamicSupport { + public function callAddAcfFieldToDefs(array $field, array &$defs, int $depth = 0): void + { + $this->addAcfFieldToDefs($field, $defs, $depth); + } + }; + } } From c87193a40a0f10fbfc17796a886ed973ab6ab5ac Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Fri, 15 May 2026 07:11:02 +0200 Subject: [PATCH 8/8] code style (WP-991) Co-Authored-By: Claude Sonnet 4.6 --- .../Extensions/Acf/AcfDynamicSupport.php | 3 +-- .../Extensions/Acf/AcfDynamicSupportTest.php | 20 +++++++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php index 57dd9e04..67a1280a 100644 --- a/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php +++ b/inc/Smartling/Extensions/Acf/AcfDynamicSupport.php @@ -25,6 +25,7 @@ class AcfDynamicSupport public const POST_TYPE_FIELD = 'acf-field'; public const POST_TYPE_GROUP = 'acf-field-group'; + private const MAX_ACF_FIELD_DEPTH = 10; public const REFERENCED_TYPE_NONE = 'none'; public const REFERENCED_TYPE_MEDIA = 'media'; public const REFERENCED_TYPE_POST = 'post'; @@ -145,8 +146,6 @@ private function collectAcfDefinitions(): array return $defs; } - private const MAX_ACF_FIELD_DEPTH = 16; - protected function addAcfFieldToDefs(array $field, array &$defs, int $depth = 0): void { if (!isset($field['key'], $field['type'])) { diff --git a/tests/Smartling/Extensions/Acf/AcfDynamicSupportTest.php b/tests/Smartling/Extensions/Acf/AcfDynamicSupportTest.php index 64113e89..532bc4cc 100644 --- a/tests/Smartling/Extensions/Acf/AcfDynamicSupportTest.php +++ b/tests/Smartling/Extensions/Acf/AcfDynamicSupportTest.php @@ -19,7 +19,7 @@ protected function setUp(): void parent::setUp(); } - public function testGetReplacerIdForField() + public function testGetReplacerIdForField(): void { $x = new class( new ArrayHelper(), @@ -39,7 +39,7 @@ public function run(): void )); } - public function testSyncFieldGroup() + public function testSyncFieldGroup(): void { $sourceBlogId = 1; $targetBlogId = 7; @@ -137,7 +137,7 @@ public function testSyncFieldGroup() $x->syncAcfData($fieldGroupSubmission); } - public function testGetRuleId() + public function testGetRuleId(): void { $x = new AcfDynamicSupport( $this->createMock(ArrayHelper::class), @@ -150,13 +150,13 @@ public function testGetRuleId() $this->assertEquals('field_66d08bd321aee', $x->getRuleId('field_66d0680a343ff_field_66d08bd321aee'), 'Should return last part of complex rule id'); } - public function testAddAcfFieldToDefsRecursesInlineSubFieldsAndLayouts() + public function testAddAcfFieldToDefsRecursesInlineSubFieldsAndLayouts(): void { $x = $this->getAcfDynamicSupportWithExposedAddField(); - // Repeater carrying its sub_fields inline (Bluebeam-style import where children - // are not stored as separate acf-field posts). ID is 0 so the DB recursion is - // skipped and we exercise only the inline path. + // Repeater carrying its sub_fields inline (Children + // are not stored as separate acf-field posts). ID is 0, so the DB recursion is + // skipped, and we exercise only the inline path. $field = [ 'key' => 'field_repeater', 'type' => 'repeater', @@ -192,12 +192,10 @@ public function testAddAcfFieldToDefsRecursesInlineSubFieldsAndLayouts() ); } - public function testAddAcfFieldToDefsHonoursDepthGuard() + public function testAddAcfFieldToDefsHonoursDepthGuard(): void { $x = $this->getAcfDynamicSupportWithExposedAddField(); - // Build a pathologically deep repeater chain (20 levels — exceeds MAX_ACF_FIELD_DEPTH=16). - // The deepest leaf must NOT make it into defs. $leafKey = 'field_too_deep_leaf'; $field = ['key' => $leafKey, 'type' => 'image']; for ($i = 19; $i >= 0; $i--) { @@ -212,7 +210,7 @@ public function testAddAcfFieldToDefsHonoursDepthGuard() $x->callAddAcfFieldToDefs($field, $defs); $this->assertArrayHasKey('field_level_0', $defs, 'top-level container should be in defs'); - $this->assertArrayHasKey('field_level_16', $defs, 'fields up to MAX depth should be in defs'); + $this->assertArrayHasKey('field_level_10', $defs, 'fields up to MAX depth should be in defs'); $this->assertArrayNotHasKey($leafKey, $defs, 'leaf beyond depth limit must be dropped'); }