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 7dc5c86b..67a1280a 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;
@@ -26,12 +25,13 @@ 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';
public const REFERENCED_TYPE_TAXONOMY = 'taxonomy';
- public static array $acfReverseDefinitionAction = [];
+ private array $filterConfigurations = [];
private ?array $definitions = null;
@@ -47,20 +47,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,237 +94,111 @@ private function getBlogListForSearch(): array
/**
* @throws SmartlingDirectRunRuntimeException
*/
- private function getDatabaseDefinitions(): array
+ private function loadDefinitions(): 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
- {
- $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,
- ];
- }
-
- return $groups;
- }
-
- private function rawReadFields($parentId): array
- {
- return $this->getFieldsFromPosts($this->getQueryByParentId($parentId)->get_posts());
- }
-
- private function getFields(int $blogId, array $parentIds): array
+ public function getFilterConfiguration(string $key): ?array
{
- return $this->siteHelper->withBlog($blogId, function () use ($parentIds): array {
- return $this->getFieldsFromPosts($this->getQueryByParentIds($parentIds)->get_posts());
- });
+ return $this->filterConfigurations[$key] ?? null;
}
- protected function extractGroupsDefinitions(array $groups): array
+ private function collectAcfDefinitions(): array
{
$defs = [];
- foreach ($groups as $group) {
+ 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,
];
- if (array_key_exists('active', $group)) {
- $defs[$group['key']]['active'] = $group['active'];
+ $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;
}
- }
-
- return $defs;
- }
-
- protected function extractFieldDefinitions(array $fields): array
- {
- $defs = [];
-
- foreach ($fields as $field) {
- $defs[$field['key']] = [
- 'global_type' => 'field',
- 'type' => $field['type'],
- 'name' => $field['name'],
- 'parent' => $field['parent'],
- ];
-
- if ('clone' === $field['type']) {
- $defs[$field['key']]['clone'] = $field['clone'];
+ foreach (acf_get_fields($group) as $field) {
+ $this->addAcfFieldToDefs($field, $defs);
}
}
return $defs;
}
- /**
- * Get local definitions for ACF Pro ver < 5.7.12
- */
- private function getLocalDefinitionsOld(): array
+ protected function addAcfFieldToDefs(array $field, array &$defs, int $depth = 0): void
{
- $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 (!isset($field['key'], $field['type'])) {
+ 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 ($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']];
+ 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;
- }
-
- 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.');
+ if (!in_array($field['type'], ['repeater', 'group', 'flexible_content'], true)) {
+ return;
}
-
- return $defs;
- }
-
- /**
- * Reads local (PHP and JSON) ACF Definitions
- */
- private function getLocalDefinitions(): array
- {
- $defs = $this->getLocalDefinitionsOld();
-
- if (empty($defs)) {
- $defs = $this->getLocalDefinitionsNew();
+ if (isset($field['ID']) && (int)$field['ID'] > 0) {
+ foreach (acf_get_fields($field) as $child) {
+ $this->addAcfFieldToDefs($child, $defs, $depth + 1);
+ }
}
-
- 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, $depth + 1);
}
-
- 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, $depth + 1);
}
}
}
}
+ }
- 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
@@ -439,31 +299,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 {
@@ -477,6 +322,13 @@ public function run(): void
$this->tryRegisterACF();
}
+ public function runIfRequired(): void
+ {
+ if ($this->definitions === null) {
+ $this->run();
+ }
+ }
+
private function prepareFilters(): void
{
$rules = [];
@@ -508,7 +360,7 @@ private function prepareFilters(): void
}
}
- static::$acfReverseDefinitionAction = $rules;
+ $this->filterConfigurations = $rules;
}
public function getReplacerIdForField(array $attributes, string $key): ?string
@@ -535,19 +387,12 @@ 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();
}
- $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
@@ -662,43 +507,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/inc/Smartling/Extensions/Acf/AcfTypeDetector.php b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php
index 7a59156f..31f4acd8 100644
--- a/inc/Smartling/Extensions/Acf/AcfTypeDetector.php
+++ b/inc/Smartling/Extensions/Acf/AcfTypeDetector.php
@@ -5,144 +5,86 @@
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}#';
- /**
- * Default cache time (1 day)
- * @var int
- */
- public static $cacheExpireSec = 84600;
-
- /**
- * @var Cache
- */
- private $cache;
-
- /**
- * @var ContentHelper
- */
- private $contentHelper;
-
- /**
- * @return Cache
- */
- public function getCache()
- {
- return $this->cache;
- }
-
- /**
- * @param Cache $cache
- */
- public function setCache($cache)
- {
- $this->cache = $cache;
- }
- /**
- * @return ContentHelper
- */
- public function getContentHelper()
- {
- return $this->contentHelper;
- }
-
- /**
- * @param ContentHelper $contentHelper
- */
- public function setContentHelper($contentHelper)
- {
- $this->contentHelper = $contentHelper;
- }
+ private const CACHE_EXPIRE_SEC = 86400;
- private function getCacheKeyByFieldName($fieldName)
+ private function getCacheKeyByFieldName(string $fieldName): string
{
- return vsprintf('acf-field-type-cache-%s', [$fieldName]);
+ return "acf-field-type-cache-$fieldName";
}
- /**
- * AcfTypeDetector constructor.
- *
- * @param ContentHelper $contentHelper
- * @param Cache $cache
- */
- public function __construct(ContentHelper $contentHelper, Cache $cache)
+ public function __construct(
+ private AcfDynamicSupport $acfDynamicSupport,
+ private Cache $cache,
+ private ContentHelper $contentHelper,
+ )
{
- $this->setCache($cache);
- $this->setContentHelper($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->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;
}
- private function getFieldKeyFieldNameByMetaFields($fieldName, array $metadata)
+ private function getFieldKeyFieldNameByMetaFields(string $fieldName, array $metadata): ?string
{
- 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;
+ 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])) {
@@ -150,10 +92,10 @@ public function getProcessorForGutenberg($field, array $fields)
}
}
- return false;
+ return null;
}
- private function getAcfProcessor($field, $key)
+ private function getAcfProcessor(string $field, ?string $key): ?MetaFieldProcessorInterface
{
$matches = [];
preg_match_all(self::ACF_FIELD_GROUP_REGEX, $key, $matches);
@@ -162,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 3a75f147..108c2dec 100644
--- a/inc/Smartling/Services/ContentRelationsDiscoveryService.php
+++ b/inc/Smartling/Services/ContentRelationsDiscoveryService.php
@@ -518,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/AcfDynamicSupportTest.php b/tests/Smartling/Extensions/Acf/AcfDynamicSupportTest.php
index dc02b318..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),
@@ -149,4 +149,84 @@ 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(): void
+ {
+ $x = $this->getAcfDynamicSupportWithExposedAddField();
+
+ // 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',
+ '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(): void
+ {
+ $x = $this->getAcfDynamicSupportWithExposedAddField();
+
+ $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_10', $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);
+ }
+ };
+ }
}
diff --git a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php
index e20048af..9a16e71d 100644
--- a/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php
+++ b/tests/Smartling/Extensions/Acf/AcfTypeDetectorTest.php
@@ -42,12 +42,13 @@ public function testGetProcessorByMetaFields(string $fieldName, array $metaField
$cache->method('get')->willReturn(false);
$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();
@@ -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 = [