HEX
Server: Apache/2.4.66 (Debian)
System: Linux 6dfabc3b2241 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64
User: (1000)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/wpgraphql-acf/src/FieldConfig.php
<?php

namespace WPGraphQL\Acf;

use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;

class FieldConfig {

	/**
	 * @var array<mixed>
	 */
	protected $acf_field;

	/**
	 * @var array<mixed>
	 */
	protected $raw_field;

	/**
	 * @var array<mixed>
	 */
	protected $acf_field_group;

	/**
	 * @var string|null
	 */
	protected $graphql_field_group_type_name;

	/**
	 * @var string
	 */
	protected $graphql_field_name;

	/**
	 * @var \WPGraphQL\Acf\AcfGraphQLFieldType|null
	 */
	protected $graphql_field_type;

	/**
	 * @var \WPGraphQL\Acf\Registry
	 */
	protected $registry;

	/**
	 * @var string
	 */
	protected $graphql_parent_type_name;

	/**
	 * @param array<mixed>            $acf_field The ACF Field being mapped to the GraphQL Schema
	 * @param array<mixed>            $acf_field_group The ACF Field Group the field belongs to
	 * @param \WPGraphQL\Acf\Registry $registry The WPGraphQL for ACF Type Registry
	 *
	 * @throws \GraphQL\Error\Error
	 */
	public function __construct( array $acf_field, array $acf_field_group, Registry $registry ) {
		$this->raw_field                     = $acf_field;
		$this->acf_field                     = ! empty( $acf_field['key'] ) && acf_get_field( $acf_field['key'] ) ? acf_get_field( $acf_field['key'] ) : $acf_field;
		$this->acf_field_group               = $acf_field_group;
		$this->registry                      = $registry;
		$this->graphql_field_group_type_name = $this->registry->get_field_group_graphql_type_name( $this->acf_field_group );
		$this->graphql_field_name            = $this->registry->get_graphql_field_name( $this->acf_field );
		$this->graphql_field_type            = Utils::get_graphql_field_type( $this->raw_field['type'] );
	}

	/**
	 * Get the WPGraphQL for ACF TypeRegistry instance
	 */
	public function get_registry(): Registry {
		return $this->registry;
	}

	/**
	 * Get the GraphQL Type Name for the current ACF Field Group
	 */
	public function get_graphql_field_group_type_name(): ?string {
		return $this->graphql_field_group_type_name;
	}

	/**
	 * Get the AcfGraphQLFieldType definition for an ACF Field
	 */
	public function get_graphql_field_type(): ?AcfGraphQLFieldType {
		return $this->graphql_field_type;
	}

	/**
	 * @param array<mixed> $acf_field
	 * @param string|null  $prepend
	 *
	 * @throws \GraphQL\Error\Error
	 */
	public function get_parent_graphql_type_name( array $acf_field, ?string $prepend = '' ): string {
		$type_name = '';

		if ( ! empty( $acf_field['parent_layout_group'] ) ) {
			$type_name = $this->registry->get_field_group_graphql_type_name( $acf_field['parent_layout_group'] );
		} elseif ( ! empty( $acf_field['parent'] ) ) {
			$parent_field = acf_get_field( $acf_field['parent'] );
			$parent_group = acf_get_field_group( $acf_field['parent'] );
			if ( ! empty( $parent_field ) ) {
				$type_name = $this->registry->get_field_group_graphql_type_name( $parent_field );
				$type_name = $this->get_parent_graphql_type_name( $parent_field, $type_name );
			} elseif ( ! empty( $parent_group ) ) {
				$type_name = $this->registry->get_field_group_graphql_type_name( $parent_group );
				$type_name = $this->get_parent_graphql_type_name( $parent_group, $type_name );
			}
		}

		return $type_name . $prepend;
	}

	/**
	 * Get the GraphQL field name of the ACF Field
	 */
	public function get_graphql_field_name(): string {
		return $this->graphql_field_name;
	}

	/**
	 * Determine whether an ACF Field is supported by GraphQL
	 */
	protected function is_supported_field_type(): bool {
		$supported_types = Utils::get_supported_acf_fields_types();
		return ! empty( $this->acf_field['type'] ) && in_array( $this->acf_field['type'], $supported_types, true );
	}

	/**
	 * Get the description of the field for the GraphQL Schema
	 *
	 * @throws \GraphQL\Error\Error
	 */
	public function get_field_description(): string {

		$graphql_field_type = $this->get_graphql_field_type();
		$field_type_config  = ( $graphql_field_type instanceof AcfGraphQLFieldType ) ? $graphql_field_type->get_config() : [];

		// Use the explicit graphql_description, if set
		if ( ! empty( $this->acf_field['graphql_description'] ) ) {
			$description = $this->acf_field['graphql_description'];

			// else use the instructions, if set
		} elseif ( ! empty( $this->acf_field['instructions'] ) ) {
			$description = $this->acf_field['instructions'];
		} else {
			// Fallback description
			// translators: %s is the name of the ACF Field Group
			$description = sprintf(
				// translators: %1$s is the ACF Field Type and %2$s is the name of the ACF Field Group
				__( 'Field of the "%1$s" Field Type added to the schema as part of the "%2$s" Field Group', 'wpgraphql-acf' ),
				$this->acf_field['type'] ?? '',
				$this->registry->get_field_group_graphql_type_name( $this->acf_field_group )
			);
		}

		if ( isset( $field_type_config['graphql_description_after'] ) ) {
			if ( is_callable( $field_type_config['graphql_description_after'] ) ) {
				$description .= ' ' . call_user_func( $field_type_config['graphql_description_after'], $this );
			} else {
				$description .= ' ' . $field_type_config['graphql_description_after'];
			}
		}

		return $description;
	}

	/**
	 * @return array<mixed>
	 */
	public function get_acf_field(): array {
		return $this->acf_field;
	}

	/**
	 * @return array<mixed>
	 */
	public function get_raw_acf_field(): array {
		return $this->raw_field;
	}

	/**
	 * @return array<mixed>
	 */
	public function get_acf_field_group(): array {
		return $this->acf_field_group;
	}

	/**
	 * @return array<mixed>|null
	 * @throws \GraphQL\Error\Error
	 * @throws \Exception
	 */
	public function get_graphql_field_config(): ?array {

		// if the field is explicitly set to not show in graphql, leave it out of the schema
		// if the field is explicitly set to not show in graphql, leave it out of the schema
		if ( isset( $this->acf_field['show_in_graphql'] ) && false === (bool) $this->acf_field['show_in_graphql'] ) {
			return null;
		}

		// if the field is not a supported type, don't add it to the schema
		if ( ! $this->is_supported_field_type() ) {
			return null;
		}

		if ( empty( $this->graphql_field_group_type_name ) || empty( $this->graphql_field_name ) ) {
			return null;
		}

		$field_config = [
			'type'            => 'String',
			'name'            => $this->graphql_field_name,
			'description'     => $this->get_field_description(),
			'acf_field'       => $this->get_acf_field(),
			'acf_field_group' => $this->acf_field_group,
			'resolve'         => function ( $root, $args, AppContext $context, ResolveInfo $info ) {
				return $this->resolve_field( $root, $args, $context, $info );
			},
		];


		if ( ! empty( $this->acf_field['type'] ) ) {
			$graphql_field_type = $this->get_graphql_field_type();

			// If the field type overrode the resolver, use it
			if ( null !== $graphql_field_type && method_exists( $graphql_field_type, 'get_resolver' ) ) {
				$resolver                = function ( $root, $args, AppContext $context, ResolveInfo $info ) use ( $graphql_field_type ) {
					return $graphql_field_type->get_resolver( $root, $args, $context, $info, $graphql_field_type, $this );
				};
				$field_config['resolve'] = $resolver;
			}

			if ( $graphql_field_type instanceof AcfGraphQLFieldType ) {
				$field_type = $graphql_field_type->get_resolve_type( $this );
			}

			if ( empty( $field_type ) ) {
				$field_type = 'String';
			}

			// if the field type returns a connection,
			// bail and let the connection handle registration to the schema
			// and resolution
			if ( 'connection' === $field_type ) {
				$this->registry->register_field( $this->acf_field );
				return null;
			}

			// if the field type returns a NULL type,
			// bail and prevent the field from being directly mapped to the Schema
			if ( 'NULL' === $field_type ) {
				return null;
			}

			switch ( $this->acf_field['type'] ) {
				case 'color_picker':
				case 'number':
				case 'range':
				case 'group':
				case 'wysiwyg':
				case 'google_map':
				case 'link':
				case 'oembed':
				case 'radio':
				case 'date_picker':
				case 'date_time_picker':
				case 'time_picker':
				case 'flexible_content':
				case 'button_group':
				case 'repeater':
					$field_config['type'] = $field_type;
					break;
				case 'true_false':
					$field_config['type']    = $field_type;
					$field_config['resolve'] = function ( $node, array $args, AppContext $context, ResolveInfo $info ) {
						$value = $this->resolve_field( $node, $args, $context, $info );
						return (bool) $value;
					};
					break;
				case 'checkbox':
				case 'select':
					$field_config['type']    = $field_type;
					$field_config['resolve'] = function ( $node, array $args, AppContext $context, ResolveInfo $info ) {
						$value = $this->resolve_field( $node, $args, $context, $info );

						if ( empty( $value ) && ! is_array( $value ) ) {
							return null;
						}

						return is_array( $value ) ? $value : [ $value ];
					};
					break;

				default:
					$field_config['type'] = $field_type;
					break;
			}
		}

		/**
		 * Filter the graphql_field_config for an ACF Field
		 *
		 * @param array|null                 $field_config   The field config array passed to the schema registration
		 * @param \WPGraphQL\Acf\FieldConfig $instance       Instance of the FieldConfig class
		 */
		return \apply_filters( 'wpgraphql/acf/get_graphql_field_config', $field_config, $this );
	}

	/**
	 * Determine if the field should ask ACF to format the response when retrieving
	 * the field using get_field()
	 *
	 * @param string $field_type The ACF Field Type of field being resolved
	 */
	public function should_format_field_value( string $field_type ): bool {

		// @todo: filter this? And it should be done at the registry level once, not the per-field level
		$types_to_format = [
			'select',
			'wysiwyg',
			'repeater',
			'flexible_content',
			'oembed',
			'clone',
		];

		return in_array( $field_type, $types_to_format, true );
	}

	/**
	 * @param string          $selector
	 * @param string|null     $parent_field_name
	 * @param string|int|null $post_id
	 * @param bool            $should_format
	 *
	 * @return false|mixed
	 */
	protected function get_field( string $selector, ?string $parent_field_name = null, $post_id = null, bool $should_format = false ) {
		if ( ! empty( $parent_field_name ) ) {
			$value = get_sub_field( $selector, $should_format );
		} else {
			$value = get_field( $selector, $post_id, $should_format );
		}

		return $value;
	}

	/**
	 * @param mixed                                $root
	 * @param array<mixed>                         $args
	 * @param \WPGraphQL\AppContext                $context
	 * @param \GraphQL\Type\Definition\ResolveInfo $info
	 * @param array<mixed>                         $acf_field The ACF Field to resolve for
	 *
	 * @return mixed
	 */
	public function resolve_field( $root, array $args, AppContext $context, ResolveInfo $info, $acf_field = [] ) {
		$field_config = $info->fieldDefinition->config['acf_field'] ?? $this->acf_field;
		// merge the field config and the acf_field passed in
		$field_config = array_merge( $field_config, $acf_field );
		$node         = $root['node'] ?? null;
		$node_id      = $node ? Utils::get_node_acf_id( $node ) : null;
		$return_value = null;
		$field_key    = null;
		$is_cloned    = false;

		if ( ! empty( $field_config['__key'] ) ) {
			$field_key = $field_config['__key'];
			$is_cloned = true;
		} elseif ( ! empty( $field_config['key'] ) ) {
			$field_key = $field_config['key'];
		}

		if ( empty( $field_key ) ) {
			return null;
		}

		if ( $is_cloned ) {
			if ( isset( $field_config['_name'] ) && ! empty( $node_id ) ) {
				$field_key = $field_config['_name'];
			} elseif ( ! empty( $field_config['__key'] ) ) {
				$field_key = $field_config['__key'];
			}
		}


		$should_format_value = false;

		if ( ! empty( $field_config['type'] ) && $this->should_format_field_value( $field_config['type'] ) ) {
			$should_format_value = true;
		}

		if ( empty( $field_key ) ) {
			return null;
		}

		// if the field_config is empty or not an array, set it as an empty array as a fallback
		$field_config = ! empty( $field_config ) ? $field_config : [];

		// If the root being passed down already has a value
		// for the field key, let's use it to resolve
		if ( isset( $field_config['key'] ) && ! empty( $root[ $field_config['key'] ] ) ) {
			return $this->prepare_acf_field_value( $root[ $field_config['key'] ], $node, $node_id, $field_config );
		}

		// Check if the cloned field key is being used to pass values down
		if ( isset( $field_config['__key'] ) && ! empty( $root[ $field_config['__key'] ] ) ) {
			return $this->prepare_acf_field_value( $root[ $field_config['__key'] ], $node, $node_id, $field_config );
		}

		// Else check if the values are being passed down via the name
		if ( isset( $field_config['name'] ) && ! empty( $root[ '_' . $field_config['name'] ] ) ) {
			return $this->prepare_acf_field_value( $root[ '_' . $field_config['name'] ], $node, $node_id, $field_config );
		}

		// Else check if the values are being passed down via the name
		if ( isset( $field_config['name'] ) && ! empty( $root[ $field_config['name'] ] ) ) {
			return $this->prepare_acf_field_value( $root[ $field_config['name'] ], $node, $node_id, $field_config );
		}

		/**
		 * Filter the field value before resolving.
		 *
		 * @param mixed            $value     The value of the ACF Field stored on the node
		 * @param mixed            $node      The object the field is connected to
		 * @param mixed|string|int $node_id   The ACF ID of the node to resolve the field with
		 * @param array            $acf_field The ACF Field config
		 * @param bool             $format    Whether to apply formatting to the field
		 * @param string           $field_key The key of the field being resolved
		 */
		$pre_value = \apply_filters( 'wpgraphql/acf/pre_resolve_acf_field', null, $root, $node_id, $field_config, $should_format_value, $field_key );

		// If the filter has returned a value, we can return the value that was returned.
		if ( null !== $pre_value ) {
			return $pre_value;
		}

		$parent_field_name = null;
		if ( ! empty( $field_config['parent'] ) ) {
			$parent_field = acf_get_field( $field_config['parent'] );
			if ( ! empty( $parent_field['name'] ) ) {
				$parent_field_name = $parent_field['name'];
			}
		}

		// resolve block field
		if ( is_array( $node ) && isset( $node['blockName'], $node['attrs'] ) ) {
			$block = $node['attrs'];

			// Ensure the block has an ID
			if ( ! isset( $block['id'] ) ) {
				$block['id'] = uniqid( 'block_', true );
			}

			$block    = acf_prepare_block( $block );
			$block_id = acf_get_block_id( $node['attrs'] );
			$block_id = acf_ensure_block_id_prefix( $block_id );

			acf_setup_meta( $block['data'] ?? [], $block_id, true );

			$return_value = $this->get_field( $field_config['name'], $parent_field_name, $block_id, $should_format_value );
			acf_reset_meta( $block_id );

			if ( empty( $return_value ) && isset( $node['attrs']['data'][ $field_config['name'] ] ) ) {
				$return_value = $node['attrs']['data'][ $field_config['name'] ];
			}

			if ( empty( $return_value ) && ( ! empty( $parent_field_name ) && ! empty( $node['attrs']['data'][ $parent_field_name . '_' . $field_config['name'] ] ) ) ) {
				$return_value = $node['attrs']['data'][ $parent_field_name . '_' . $field_config['name'] ];
			}
		}



		// If there's no node_id at this point, we can return null
		if ( empty( $return_value ) && empty( $node_id ) ) {
			return null;
		}

		// if a value hasn't been set yet, use the get_field() function to get the value
		if ( empty( $return_value ) ) {
			$return_value = $this->get_field( $field_key, $parent_field_name, $node_id, $should_format_value );
		}

		// Prepare the value for response
		$prepared_value = $this->prepare_acf_field_value( $return_value, $root, $node_id, $field_config );
		// Empty values are set to null
		if ( empty( $prepared_value ) ) {
			$prepared_value = null;
		}

		/**
		 * Filter the value before returning
		 *
		 * @param mixed $value
		 * @param array $field_config The ACF Field Config for the field being resolved
		 * @param mixed $root The Root node or object of the field being resolved
		 * @param mixed $node_id The ID of the node being resolved
		 */
		return \apply_filters( 'wpgraphql/acf/field_value', $prepared_value, $field_config, $root, $node_id );
	}

	/**
	 * Given a value of an ACF Field, this prepares it for response by applying formatting, etc based on
	 * the field type.
	 *
	 * @param mixed            $value   The value of the ACF field to return
	 * @param mixed            $root    The root node/object the field belongs to
	 * @param mixed|string|int $node_id The ID of the node the field belongs to
	 * @param ?array           $acf_field_config The ACF Field Config for the field being resolved
	 *
	 * @return mixed
	 */
	public function prepare_acf_field_value( $value, $root, $node_id, ?array $acf_field_config = [] ) {
		if ( empty( $acf_field_config ) || ! is_array( $acf_field_config ) ) {
			return $value;
		}

		// if the value is an array and the field return format is set to array,
		// map over the array to get the return values
		if ( isset( $acf_field_config['return_format'] ) && 'array' === $acf_field_config['return_format'] && is_array( $value ) ) {
			$value = array_map(
				static function ( $opt ) {
					if ( ! is_array( $opt ) ) {
						return $opt;
					}

					return $opt['value'] ?? null;
				},
				$value
			);
		}

		if ( isset( $acf_field_config['new_lines'] ) ) {
			if ( 'wpautop' === $acf_field_config['new_lines'] ) {
				$value = \wpautop( $value );
			}
			if ( 'br' === $acf_field_config['new_lines'] ) {
				$value = \nl2br( $value );
			}
		}

		// @todo: This was ported over, but I'm not 💯 sure what this is solving and
		// why it's only applied on options pages and not other pages 🤔
		if ( 'wysiwyg' === $acf_field_config['type'] ) {
			$value = \apply_filters( 'the_content', $value );
		}

		if ( ! empty( $acf_field_config['type'] ) && in_array(
			$acf_field_config['type'],
			[
				'date_picker',
				'time_picker',
				'date_time_picker',
			],
			true
		) ) {
			if ( ! empty( $value ) && ! empty( $acf_field_config['return_format'] ) ) {
				$value = gmdate( $acf_field_config['return_format'], strtotime( $value ) );
			}
		}

		if ( ! empty( $acf_field_config['type'] ) && in_array( $acf_field_config['type'], [ 'number', 'range' ], true ) ) {
			$value = (float) $value ?: null;
		}

		return $value;
	}

	/**
	 * @param string $to_type
	 */
	public function get_connection_name( string $to_type ): string {
		return \WPGraphQL\Utils\Utils::format_type_name( 'Acf' . ucfirst( $to_type ) . 'Connection' );
	}

	/**
	 * @param array<mixed> $config The Connection Config to use
	 *
	 * @throws \Exception
	 */
	public function register_graphql_connections( array $config ): void {
		$type_name = $this->get_graphql_field_group_type_name();
		$to_type   = $config['toType'] ?? null;

		// If there's no to_type or type_name, we can't proceed
		if ( empty( $to_type ) || empty( $type_name ) ) {
			return;
		}

		$connection_name = $this->get_connection_name( $to_type );

		$connection_config = array_merge(
			[
				'description'           => $this->get_field_description(),
				'acf_field'             => $this->get_acf_field(),
				'acf_field_group'       => $this->get_acf_field_group(),
				'fromType'              => $type_name,
				'toType'                => $to_type,
				'connectionTypeName'    => $connection_name,
				'fromFieldName'         => $this->get_graphql_field_name(),
				'allowFieldUnderscores' => true,
			],
			$config
		);

		$connection_key = $this->get_graphql_field_name() . ':' . $type_name . ':' . $to_type;

		if ( $this->registry->has_registered_field_group( $connection_key ) ) {
			return;
		}

		// Register the connection to the Field Group Type
		if ( defined( 'WPGRAPHQL_VERSION' ) && version_compare( WPGRAPHQL_VERSION, '1.23.0', '<=' ) ) {
			register_graphql_connection( $connection_config );
		}

		// Register the connection to the Field Group Fields Interface
		register_graphql_connection( array_merge( $connection_config, [ 'fromType' => $type_name . '_Fields' ] ) );

		$this->registry->register_field_group( $connection_key, $connection_config );
	}
}