From 6b40be9e9cf67f7803072755711a16202cf592d4 Mon Sep 17 00:00:00 2001 From: Alaa Date: Sun, 26 Mar 2023 16:10:40 +0300 Subject: [PATCH 1/9] feat: Support mutations for ACF fields --- src/class-config.php | 10 +- src/class-mutations.php | 579 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 587 insertions(+), 2 deletions(-) create mode 100644 src/class-mutations.php diff --git a/src/class-config.php b/src/class-config.php index 739d2bb..e4d9bf0 100644 --- a/src/class-config.php +++ b/src/class-config.php @@ -76,6 +76,12 @@ public function init( TypeRegistry $type_registry ) { $this->add_options_pages_to_schema(); $this->add_acf_fields_to_graphql_types(); + /** + * Add ACF Fields to GraphQL mutations + */ + $mutations_obj = new Mutations(); + $mutations_obj->init( $type_registry, $this ); + // This filter tells WPGraphQL to resolve revision meta for ACF fields from the revision's meta, instead // of the parent (published post) meta. add_filter( 'graphql_resolve_revision_meta_from_parent', function( $should, $object_id, $meta_key, $single ) { @@ -167,7 +173,7 @@ public function register_initial_types() { * Gets the location rules * @return array */ - protected function get_location_rules() { + public static function get_location_rules() { $field_groups = acf_get_field_groups(); if ( empty( $field_groups ) || ! is_array( $field_groups ) ) { @@ -296,7 +302,7 @@ protected function add_options_pages_to_schema() { * * @return bool */ - protected function should_field_group_show_in_graphql( $field_group ) { + public function should_field_group_show_in_graphql( $field_group ) { /** * By default, field groups will not be exposed to GraphQL. diff --git a/src/class-mutations.php b/src/class-mutations.php new file mode 100644 index 0000000..41c3a7f --- /dev/null +++ b/src/class-mutations.php @@ -0,0 +1,579 @@ +type_registry = $type_registry; + $this->config = $config; + + /** + * Get all the field groups + */ + $this->field_groups = acf_get_field_groups(); + + /** + * If there are no acf field groups, bail + */ + if ( empty( $this->field_groups ) || ! is_array( $this->field_groups ) ) { + return; + } + + /** + * Gets the location rules for those fields that do not have "graphql_type". + * + * This allows for ACF Field Groups that were registered before the "graphql_types" ( backward compatibility ) + * or registered from code can still work with the old GraphQL Schema rules that mapped + * from the ACF Location rules. + */ + $this->location_rules = Config::get_location_rules(); + + /** + * Add ACF Fields to GraphQL Mutations + */ + $this->add_acf_fields_to_graphql_types(); + + add_action( 'graphql_post_object_mutation_update_additional_data', function ( int $post_id, array $input ) { + $this->save_registered_fields_data( $post_id, self::POST_OBJECT_TYPE, $input, $this->registered_fields ); + }, 10, 2 ); + + add_filter( 'graphql_term_object_insert_term_args', function ( array $insert_args, array $input ) { + self::add_action_once( 'graphql_insert_term', function ( int $term_id ) use ( $input ) { + $this->save_registered_fields_data( $term_id, self::TERM_OBJECT_TYPE, $input, $this->registered_fields ); + } ); + self::add_action_once( 'graphql_update_term', function ( int $term_id ) use ( $input ) { + $this->save_registered_fields_data( $term_id, self::TERM_OBJECT_TYPE, $input, $this->registered_fields ); + } ); + + return $insert_args; + }, 10, 2 ); + } + + private function save_registered_fields_data( int $object_id, string $object_type, array $fields_data, array $registered_fields ): void + { + foreach ( $fields_data as $key => $value ) { + if ( ! empty( $registered_fields[$key] ) ) { + if ( ! empty( $registered_fields[$key]['sub_fields_config'] ) ) { + $this->save_registered_fields_data( $object_id, $object_type, $value, $registered_fields[$key]['sub_fields_config'] ); + } + else if ( + ! empty( $registered_fields[$key]['mutate'] ) + && is_callable( $registered_fields[$key]['mutate'] ) + ) { + call_user_func_array( $registered_fields[$key]['mutate'], [ $object_id, $object_type, $value, $registered_fields[$key] ] ); + } + else { + $this->update_acf_field_value( $object_id, $object_type, $value, $registered_fields[$key] ); + } + } + } + } + + /** + * Given a field group array, this adds the fields to the specified Type in the Schema + * + * @param array $field_group The group to add to the Schema. + * @param bool $layout Whether or not these fields are part of a Flex Content layout. + * + * @return array|null + */ + private function add_field_group_fields( array $field_group, bool $layout = false ) { + + /** + * If the field group has the show_in_graphql setting configured, respect it's setting + * otherwise default to true (for nested fields) + */ + $field_group['show_in_graphql'] = isset( $field_group['show_in_graphql'] ) ? (boolean) $field_group['show_in_graphql'] : true; + + /** + * Determine if the field group should be exposed + * to graphql + */ + if ( ! $this->config->should_field_group_show_in_graphql( $field_group ) ) { + return null; + } + + /** + * Get the fields in the group. + */ + $acf_fields = ! empty( $field_group['sub_fields'] ) || $layout ? $field_group['sub_fields'] : acf_get_fields( $field_group ); + + /** + * If there are no fields, bail + */ + if ( empty( $acf_fields ) || ! is_array( $acf_fields ) ) { + return null; + } + + /** + * Stores field keys to prevent duplicate field registration for cloned fields + */ + $processed_keys = []; + + $registered_fields = []; + /** + * Loop over the fields and register them to the Schema + */ + foreach ( $acf_fields as $acf_field ) { + if ( in_array( $acf_field['key'], $processed_keys, true ) ) { + continue; + } else { + $processed_keys[] = $acf_field['key']; + } + + /** + * Setup data for register_graphql_field + */ + $explicit_name = ! empty( $acf_field['graphql_field_name'] ) ? $acf_field['graphql_field_name'] : null; + $name = empty( $explicit_name ) && ! empty( $acf_field['name'] ) ? Config::camel_case( $acf_field['name'] ) : $explicit_name; + $show_in_graphql = isset( $acf_field['show_in_graphql'] ) ? (bool) $acf_field['show_in_graphql'] : true; + $description = isset( $acf_field['instructions'] ) ? $acf_field['instructions'] : __( 'ACF Field added to the Schema by WPGraphQL ACF' ); + + /** + * If the field is missing a name or a type, + * we can't add it to the Schema. + */ + if ( + empty( $name ) || + true != $show_in_graphql + ) { + + /** + * Uncomment line below to determine what fields are not going to be output + * in the Schema. + */ + continue; + } + + $config = [ + 'name' => $name, + 'description' => $description, + 'acf_field' => $acf_field, + 'acf_field_group' => $field_group, + ]; + + $field_config = $this->register_graphql_field( $name, $config ); + if ( ! empty( $field_config ) ) { + $registered_fields[$name] = $field_config; + } + + } + + return $registered_fields; + } + + private function get_field_key( $acf_field ) { + /** + * Check if cloned field and retrieve the key accordingly. + */ + if ( ! empty( $acf_field['_clone'] ) ) { + $key = $acf_field['__key']; + } else { + $key = $acf_field['key']; + } + + return $key; + } + + private function maybe_filter_value( $field_config, $value ) { + $original_value = $value; + $filtered_value = $value; + + if ( + ! empty( $field_config['filter_value'] ) + && is_callable( $field_config['filter_value'] ) + ) { + $filtered_value = call_user_func_array( $field_config['filter_value'], [ $filtered_value ] ); + } + + $acf_type = $field_config['acf_field']['type']; + $field_name = $field_config['name']; + + return apply_filters( 'wpgraphql_acf_filter_mutation_field_value', $filtered_value, $acf_type, $original_value, $field_name, $field_config ); + } + + private function update_acf_field_value( int $object_id, string $object_type, $value, array $field_config, bool $use_add_row = false ) { + + switch ( $object_type ) { + case self::TERM_OBJECT_TYPE: + $object_id = 'term_' . $object_id; + break; + case self::POST_OBJECT_TYPE: + // do nothing in this case + break; +// case $root instanceof MenuItem: +// $id = absint( $root->menuItemId ); +// break; +// case $root instanceof Menu: +// $id = 'term_' . $root->menuId; +// break; +// case $root instanceof User: +// $id = 'user_' . absint( $root->userId ); +// break; +// case $root instanceof Comment: +// $id = 'comment_' . absint( $root->databaseId ); +// break; +// case is_array( $root ) && ! empty( $root['type'] ) && 'options_page' === $root['type']: +// $id = $root['post_id']; +// break; + default: + $object_id = null; + break; + } + + if ( empty( $object_id ) ) { + return null; + } + + $acf_field = $field_config['acf_field']; + $key = $this->get_field_key( $acf_field ); + + $value = $this->maybe_filter_value( $field_config, $value ); + + if ( $value !== null ) { + if ( $use_add_row ) { + add_row( $key, $value, $object_id ); + } + else { + update_field( $key, $value, $object_id ); + } + } + } + + private function prepare_input_type_name( string $acf_field_name ): string { + $field_type_name = 'ACF_' . ucfirst( Config::camel_case( $acf_field_name ) ) . 'Input'; + + $counter = 1; + while ( null !== $this->type_registry->get_type( $field_type_name ) ) { + $field_type_name .= "_{$counter}"; + $counter++; + } + + return $field_type_name; + } + + /** + * Undocumented function + * + * @param string $field_name The name of the field to add to the GraphQL Type. + * @param array $config The GraphQL configuration of the field. + * + * @return array|null + */ + private function register_graphql_field( string $field_name, array $config ) { + $acf_field = isset( $config['acf_field'] ) ? $config['acf_field'] : null; + $acf_type = isset( $acf_field['type'] ) ? $acf_field['type'] : null; + + if ( empty( $acf_type ) ) { + return null; + } + + $field_config = [ + 'type' => null, + ]; + + switch ( $acf_type ) { + case 'button_group': + case 'color_picker': + case 'email': + case 'text': + case 'message': + case 'oembed': + case 'password': + case 'wysiwyg': + case 'url': + case 'textarea': + case 'radio': + $field_config['type'] = 'String'; + break; + case 'select': + + /** + * If the select field is configured to not allow multiple values + * the field will accept a string, but if it is configured to allow + * multiple values it will accept a list of strings + */ + if ( empty( $acf_field['multiple'] ) ) { + $field_config['type'] = 'String'; + } else { + $field_config['type'] = [ 'list_of' => 'String' ]; + } + break; + case 'number': + case 'range': + $field_config['type'] = 'Float'; + break; + case 'true_false': + $field_config['type'] = 'Boolean'; + break; + case 'date_picker': + case 'time_picker': + case 'date_time_picker': + $field_config = [ + 'type' => 'String', + 'filter_value' => function( $value ) use ( $acf_type ) { + $timestamp = strtotime( $value ); + if ( $timestamp !== false ) { + switch ( $acf_type ) { + case 'time_picker': + $value = gmdate( 'H:i:s', $timestamp ); + break; + case 'date_picker': + $value = gmdate( 'Y-m-d', $timestamp ); + break; + default:// 'date_time_picker' + $value = gmdate( 'Y-m-d H:i:s', $timestamp ); + break; + } + + return $value; + } + return null; + } + ]; + break; + case 'relationship': + $field_config['type'] = [ 'list_of' => 'ID' ]; + break; + case 'image': + case 'file': + $field_config = [ + 'type' => 'String', + 'filter_value' => function( $value ) { + $attachment_url = $value; + if ( ! empty( $attachment_url ) ) { + $attach_id = attachment_url_to_postid( $attachment_url ); + + if ( ! empty( $attach_id ) ) { + return $attach_id; + } + } + return null; + }, + ]; + break; + case 'checkbox': + $field_config['type'] = [ 'list_of' => 'String' ]; + break; + case 'taxonomy': + $is_multiple = isset( $acf_field['field_type'] ) && in_array( $acf_field['field_type'], [ 'checkbox', 'multi_select' ] ); + + $field_config['type'] = $is_multiple ? [ 'list_of' => 'ID' ] : 'ID'; + break; + // Accordions are not represented in the GraphQL Schema. + case 'accordion': + $field_config = null; + break; + case 'group': + + $field_type_name = $this->prepare_input_type_name( $acf_field['name'] ); + + $sub_fields_config = $this->add_field_group_fields( $acf_field ); + + if ( ! empty( $sub_fields_config ) ) { + $this->type_registry->register_input_type( + $field_type_name, + [ + 'description' => __( 'Field Group', 'wp-graphql-acf' ), + 'fields' => $sub_fields_config, + ] + ); + + $field_config = [ + 'type' => $field_type_name, + 'sub_fields_config' => $sub_fields_config, + ]; + } + break; + case 'repeater': + + $field_type_name = $this->prepare_input_type_name( $acf_field['name'] ); + + $sub_fields_config = $this->add_field_group_fields( $acf_field ); + + if ( ! empty( $sub_fields_config ) ) { + $this->type_registry->register_input_type( + $field_type_name, + [ + 'description' => __( 'Field Group', 'wp-graphql-acf' ), + 'fields' => $sub_fields_config, + ] + ); + + $field_config = [ + 'type' => [ 'list_of' => $field_type_name ], + 'repeater_sub_fields_config' => $sub_fields_config, + 'mutate' => function( $object_id, $object_type, $rows, $field_config ) use ( $sub_fields_config ) { + if ( ! empty( $rows ) && is_array( $rows ) ) { + foreach ( $rows as $row ) { + $row_value = []; + foreach ( $row as $sub_field_key => $sub_field_value ) { + $sub_field_config = $sub_fields_config[$sub_field_key]; + if ( ! empty( $sub_field_config ) ) { + $sub_field_value = $this->maybe_filter_value( $sub_field_config, $sub_field_value ); + + if ( $sub_field_value !== null ) { + $acf_key = $this->get_field_key( $sub_field_config['acf_field'] ); + $row_value[$acf_key] = $sub_field_value; + } + } + } + + if ( ! empty( $row_value ) ) { + $this->update_acf_field_value( $object_id, $object_type, $row_value, $field_config, true ); + } + } + } + }, + ]; + } + break; +// case 'page_link': +// case 'post_object': +// case 'link': +// case 'gallery': +// case 'user': +// case 'google_map': +// case 'flexible_content': +// break; + default: + break; + } + + if ( empty( $field_config ) || empty( $field_config['type'] ) ) { + return null; + } + + return array_merge( $config, $field_config ); + } + + /** + * Adds acf field groups to GraphQL Mutations. + */ + private function add_acf_fields_to_graphql_types() { + /** + * Loop over all the field groups + */ + foreach ( $this->field_groups as $field_group ) { + + $field_group_name = isset( $field_group['graphql_field_name'] ) ? $field_group['graphql_field_name'] : $field_group['title']; + $field_group_name = Utils::format_field_name( $field_group_name ); + + $manually_set_graphql_types = isset( $field_group['map_graphql_types_from_location_rules'] ) ? (bool) $field_group['map_graphql_types_from_location_rules'] : false; + + if ( false === $manually_set_graphql_types ) { + if ( ! isset( $field_group['graphql_types'] ) || empty( $field_group['graphql_types'] ) ) { + $field_group['graphql_types'] = []; + if ( isset( $this->location_rules[ $field_group_name ] ) ) { + $field_group['graphql_types'] = $this->location_rules[ $field_group_name ]; + } + } + } + + if ( ! is_array( $field_group['graphql_types'] ) || empty( $field_group['graphql_types'] ) ) { + continue; + } + + /** + * Determine if the field group should be exposed + * to graphql + */ + if ( ! $this->config->should_field_group_show_in_graphql( $field_group ) ) { + continue; + } + + $graphql_types = array_unique( $field_group['graphql_types'] ); + $graphql_types = array_filter( $graphql_types ); + + /** + * Prepare default info + */ + $field_name = isset( $field_group['graphql_field_name'] ) ? $field_group['graphql_field_name'] : Config::camel_case( $field_group['title'] ); + $field_group['type'] = 'group'; + $field_group['name'] = $field_name; + $config = [ + 'name' => $field_name, + 'acf_field' => $field_group, + 'acf_field_group' => null, + ]; + + $qualifier = sprintf( __( 'Added to the GraphQL Schema because the ACF Field Group "%1$s" was set to Show in GraphQL.', 'wp-graphql-acf' ), $field_group['title'] ); + $config['description'] = $field_group['description'] ? $field_group['description'] . ' | ' . $qualifier : $qualifier; + + $field_config = $this->register_graphql_field( $field_name, $config ); + if ( ! empty( $field_config ) ) { + /** + * Loop over the GraphQL types for this field group on + */ + foreach ( $graphql_types as $graphql_type ) { + $this->type_registry->register_field( "Create{$graphql_type}Input", $field_name, $field_config ); + $this->type_registry->register_field( "Update{$graphql_type}Input", $field_name, $field_config ); + } + + $this->registered_fields[$field_name] = $field_config; + } + } + } + + /** + * Register an action to run exactly one time. + * + * The arguments match that of add_action(), but this function will also register a second + * callback designed to remove the first immediately after it runs. + * + * @param string $hook_name The name of the action to add the callback to. + * @param callable $callback The callback to be run when the action is called. + * @param int $priority Optional. Used to specify the order in which the functions + * associated with a particular action are executed. + * Lower numbers correspond with earlier execution, + * and functions with the same priority are executed + * in the order in which they were added to the action. Default 10. + * @param int $accepted_args Optional. The number of arguments the function accepts. Default 1. + * @return bool Like add_action(), this function always returns true. + */ + public static function add_action_once( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): bool { + $singular = function () use ( $hook_name, $callback, $priority, $accepted_args, &$singular ) { + remove_action( $hook_name, $singular, $priority ); + call_user_func_array( $callback, func_get_args() ); + }; + + return add_action( $hook_name, $singular, $priority, $accepted_args ); + } +} From fcf04c40b6f8f32194bb465271658f389dbe8547 Mon Sep 17 00:00:00 2001 From: Alaa Date: Tue, 18 Apr 2023 10:29:54 +0300 Subject: [PATCH 2/9] feat: Make the input type relative to its parent --- src/class-mutations.php | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/class-mutations.php b/src/class-mutations.php index 41c3a7f..49850f9 100644 --- a/src/class-mutations.php +++ b/src/class-mutations.php @@ -114,7 +114,7 @@ private function save_registered_fields_data( int $object_id, string $object_typ * * @return array|null */ - private function add_field_group_fields( array $field_group, bool $layout = false ) { + private function add_field_group_fields( array $field_group, string $parent_type_name, bool $layout = false ) { /** * If the field group has the show_in_graphql setting configured, respect it's setting @@ -189,7 +189,7 @@ private function add_field_group_fields( array $field_group, bool $layout = fals 'acf_field_group' => $field_group, ]; - $field_config = $this->register_graphql_field( $name, $config ); + $field_config = $this->register_graphql_field( $config, $parent_type_name ); if ( ! empty( $field_config ) ) { $registered_fields[$name] = $field_config; } @@ -277,27 +277,23 @@ private function update_acf_field_value( int $object_id, string $object_type, $v } } - private function prepare_input_type_name( string $acf_field_name ): string { - $field_type_name = 'ACF_' . ucfirst( Config::camel_case( $acf_field_name ) ) . 'Input'; - - $counter = 1; - while ( null !== $this->type_registry->get_type( $field_type_name ) ) { - $field_type_name .= "_{$counter}"; - $counter++; + private function prepare_input_type_name( string $acf_field_name, string $parent_type_name ): string { + $prefix = ''; + if ( ! empty( $parent_type_name ) ) { + $prefix = "{$parent_type_name}_"; } - return $field_type_name; + return $prefix . ucfirst( Config::camel_case( $acf_field_name ) ) . 'Input'; } /** * Undocumented function * - * @param string $field_name The name of the field to add to the GraphQL Type. * @param array $config The GraphQL configuration of the field. * * @return array|null */ - private function register_graphql_field( string $field_name, array $config ) { + private function register_graphql_field( array $config, string $parent_type_name = '' ) { $acf_field = isset( $config['acf_field'] ) ? $config['acf_field'] : null; $acf_type = isset( $acf_field['type'] ) ? $acf_field['type'] : null; @@ -403,9 +399,9 @@ private function register_graphql_field( string $field_name, array $config ) { break; case 'group': - $field_type_name = $this->prepare_input_type_name( $acf_field['name'] ); + $field_type_name = $this->prepare_input_type_name( $acf_field['name'], $parent_type_name ); - $sub_fields_config = $this->add_field_group_fields( $acf_field ); + $sub_fields_config = $this->add_field_group_fields( $acf_field, $field_type_name ); if ( ! empty( $sub_fields_config ) ) { $this->type_registry->register_input_type( @@ -424,9 +420,9 @@ private function register_graphql_field( string $field_name, array $config ) { break; case 'repeater': - $field_type_name = $this->prepare_input_type_name( $acf_field['name'] ); + $field_type_name = $this->prepare_input_type_name( $acf_field['name'], $parent_type_name ); - $sub_fields_config = $this->add_field_group_fields( $acf_field ); + $sub_fields_config = $this->add_field_group_fields( $acf_field, $field_type_name ); if ( ! empty( $sub_fields_config ) ) { $this->type_registry->register_input_type( @@ -537,7 +533,7 @@ private function add_acf_fields_to_graphql_types() { $qualifier = sprintf( __( 'Added to the GraphQL Schema because the ACF Field Group "%1$s" was set to Show in GraphQL.', 'wp-graphql-acf' ), $field_group['title'] ); $config['description'] = $field_group['description'] ? $field_group['description'] . ' | ' . $qualifier : $qualifier; - $field_config = $this->register_graphql_field( $field_name, $config ); + $field_config = $this->register_graphql_field( $config ); if ( ! empty( $field_config ) ) { /** * Loop over the GraphQL types for this field group on From a3069cd3795d9d16bd6cf8eda22c0d663c1e3431 Mon Sep 17 00:00:00 2001 From: mohjak Date: Wed, 26 Jul 2023 18:12:32 +0300 Subject: [PATCH 3/9] feat: register choices of acf fields as graphql enum types --- src/class-config.php | 50 +++++++++++++++++++++++++++++++++++++---- src/class-mutations.php | 13 ++++++++--- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/class-config.php b/src/class-config.php index e4d9bf0..e8192a9 100644 --- a/src/class-config.php +++ b/src/class-config.php @@ -602,19 +602,21 @@ protected function register_graphql_field( string $type_name, string $field_name * * @see: https://github.com/wp-graphql/wp-graphql-acf/issues/25 */ + $enum_type = $this->register_choices_of_acf_fields_as_enum_type( $acf_field ); + $enum_type = ! empty( $enum_type ) ? $enum_type : 'String'; if ( empty( $acf_field['multiple'] ) ) { if('array' === $acf_field['return_format'] ){ - $field_config['type'] = [ 'list_of' => 'String' ]; + $field_config['type'] = [ 'list_of' => $enum_type ]; $field_config['resolve'] = function( $root ) use ( $acf_field) { $value = $this->get_acf_field_value( $root, $acf_field, true); return ! empty( $value ) && is_array( $value ) ? $value : []; }; }else{ - $field_config['type'] = 'String'; + $field_config['type'] = $enum_type; } } else { - $field_config['type'] = [ 'list_of' => 'String' ]; + $field_config['type'] = [ 'list_of' => $enum_type ]; $field_config['resolve'] = function( $root ) use ( $acf_field ) { $value = $this->get_acf_field_value( $root, $acf_field ); @@ -623,7 +625,9 @@ protected function register_graphql_field( string $type_name, string $field_name } break; case 'radio': - $field_config['type'] = 'String'; + $enum_type = $this->register_choices_of_acf_fields_as_enum_type( $acf_field ); + $enum_type = ! empty( $enum_type ) ? $enum_type : 'String'; + $field_config['type'] = $enum_type; break; case 'number': case 'range': @@ -1501,4 +1505,42 @@ protected function add_acf_fields_to_graphql_types() { } + public function register_choices_of_acf_fields_as_enum_type( array $acf_field ): string { + // If the field isn't a select or radio field, return an empty string. + if ( 'select' !== $acf_field['type'] && 'radio' !== $acf_field['type'] ) { + return ''; + } + + // Generate a unique name for the enum type using the field key. + $enum_type_name = ucfirst( self::camel_case( $acf_field['name'] ) ) . 'Enum'; + if ( ! $this->type_registry->has_type( $enum_type_name ) ) { + // Initialize an empty array to hold your enum values. + $enum_values = []; + + // Loop over the choices in the field and add them to the enum values array. + foreach ( $acf_field['choices'] as $key => $choice ) { + // Use the sanitize_key function to create a valid enum name from the choice key. + $enum_key = strtoupper( sanitize_key( $key ) ); + + // Add the choice to the enum values array. + $enum_values[ $enum_key ] = [ + 'value' => $key, + 'description' => $choice, + ]; + } + + // Register enum type. + $this->type_registry->register_enum_type( + $enum_type_name, + [ + 'description' => $acf_field['label'], + 'values' => $enum_values, + ] + ); + } + + // Return the enum type name. + return $enum_type_name; + } + } diff --git a/src/class-mutations.php b/src/class-mutations.php index 49850f9..9548209 100644 --- a/src/class-mutations.php +++ b/src/class-mutations.php @@ -316,9 +316,14 @@ private function register_graphql_field( array $config, string $parent_type_name case 'wysiwyg': case 'url': case 'textarea': - case 'radio': $field_config['type'] = 'String'; break; + + case 'radio': + $enum_type = $this->config->register_choices_of_acf_fields_as_enum_type( $acf_field ); + $enum_type = ! empty( $enum_type ) ? $enum_type : 'String'; + $field_config['type'] = $enum_type; + break; case 'select': /** @@ -326,10 +331,12 @@ private function register_graphql_field( array $config, string $parent_type_name * the field will accept a string, but if it is configured to allow * multiple values it will accept a list of strings */ + $enum_type = $this->config->register_choices_of_acf_fields_as_enum_type( $acf_field ); + $enum_type = ! empty( $enum_type ) ? $enum_type : 'String'; if ( empty( $acf_field['multiple'] ) ) { - $field_config['type'] = 'String'; + $field_config['type'] = $enum_type; } else { - $field_config['type'] = [ 'list_of' => 'String' ]; + $field_config['type'] = [ 'list_of' => $enum_type ]; } break; case 'number': From 39f2fa15d2a76e7ee1c4a2532bd31ad2d68f9993 Mon Sep 17 00:00:00 2001 From: mohjak Date: Wed, 26 Jul 2023 18:32:53 +0300 Subject: [PATCH 4/9] feat: upgrade plugin version to 0.6.2 --- wp-graphql-acf.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wp-graphql-acf.php b/wp-graphql-acf.php index 520764a..f358e7c 100644 --- a/wp-graphql-acf.php +++ b/wp-graphql-acf.php @@ -7,7 +7,7 @@ * Author URI: https://www.wpgraphql.com * Text Domain: wp-graphql-acf * Domain Path: /languages - * Version: 0.6.1 + * Version: 0.6.2 * Requires PHP: 7.0 * GitHub Plugin URI: https://github.com/wp-graphql/wp-graphql-acf * @@ -26,7 +26,7 @@ * Define constants */ const WPGRAPHQL_REQUIRED_MIN_VERSION = '0.4.0'; -const WPGRAPHQL_ACF_VERSION = '0.6.1'; +const WPGRAPHQL_ACF_VERSION = '0.6.2'; /** * Initialize the plugin From dabc314806413cc3f1b956ebff4a3242f98986ac Mon Sep 17 00:00:00 2001 From: mohjak Date: Thu, 27 Jul 2023 09:22:43 +0300 Subject: [PATCH 5/9] Revert "feat: upgrade plugin version to 0.6.2" This reverts commit 39f2fa15d2a76e7ee1c4a2532bd31ad2d68f9993. --- wp-graphql-acf.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wp-graphql-acf.php b/wp-graphql-acf.php index f358e7c..520764a 100644 --- a/wp-graphql-acf.php +++ b/wp-graphql-acf.php @@ -7,7 +7,7 @@ * Author URI: https://www.wpgraphql.com * Text Domain: wp-graphql-acf * Domain Path: /languages - * Version: 0.6.2 + * Version: 0.6.1 * Requires PHP: 7.0 * GitHub Plugin URI: https://github.com/wp-graphql/wp-graphql-acf * @@ -26,7 +26,7 @@ * Define constants */ const WPGRAPHQL_REQUIRED_MIN_VERSION = '0.4.0'; -const WPGRAPHQL_ACF_VERSION = '0.6.2'; +const WPGRAPHQL_ACF_VERSION = '0.6.1'; /** * Initialize the plugin From 52c801ed7cd98eb4760fa8442bff338af30957ff Mon Sep 17 00:00:00 2001 From: mohjak Date: Thu, 27 Jul 2023 11:28:41 +0300 Subject: [PATCH 6/9] fix: add empty check of empty choices, some minor enhancements --- src/class-config.php | 68 ++++++++++++++++++++--------------------- src/class-mutations.php | 13 +++----- 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/class-config.php b/src/class-config.php index e8192a9..8ca5a0b 100644 --- a/src/class-config.php +++ b/src/class-config.php @@ -602,21 +602,20 @@ protected function register_graphql_field( string $type_name, string $field_name * * @see: https://github.com/wp-graphql/wp-graphql-acf/issues/25 */ - $enum_type = $this->register_choices_of_acf_fields_as_enum_type( $acf_field ); - $enum_type = ! empty( $enum_type ) ? $enum_type : 'String'; + $field_type = $this->register_choices_of_acf_fields_as_enum_type( $acf_field ); if ( empty( $acf_field['multiple'] ) ) { if('array' === $acf_field['return_format'] ){ - $field_config['type'] = [ 'list_of' => $enum_type ]; + $field_config['type'] = [ 'list_of' => $field_type ]; $field_config['resolve'] = function( $root ) use ( $acf_field) { $value = $this->get_acf_field_value( $root, $acf_field, true); return ! empty( $value ) && is_array( $value ) ? $value : []; }; }else{ - $field_config['type'] = $enum_type; + $field_config['type'] = $field_type; } } else { - $field_config['type'] = [ 'list_of' => $enum_type ]; + $field_config['type'] = [ 'list_of' => $field_type ]; $field_config['resolve'] = function( $root ) use ( $acf_field ) { $value = $this->get_acf_field_value( $root, $acf_field ); @@ -625,9 +624,8 @@ protected function register_graphql_field( string $type_name, string $field_name } break; case 'radio': - $enum_type = $this->register_choices_of_acf_fields_as_enum_type( $acf_field ); - $enum_type = ! empty( $enum_type ) ? $enum_type : 'String'; - $field_config['type'] = $enum_type; + $field_type = $this->register_choices_of_acf_fields_as_enum_type( $acf_field ); + $field_config['type'] = $field_type; break; case 'number': case 'range': @@ -990,79 +988,79 @@ protected function register_graphql_field( string $type_name, string $field_name // ACF 5.8.6 added more data to Google Maps field value // https://www.advancedcustomfields.com/changelog/ if ( \acf_version_compare(acf_get_db_version(), '>=', '5.8.6' ) ) { - $fields += [ - 'streetName' => [ + $fields += [ + 'streetName' => [ 'type' => 'String', 'description' => __( 'The street name associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['street_name'] ) ? $root['street_name'] : null; }, - ], - 'streetNumber' => [ + ], + 'streetNumber' => [ 'type' => 'String', 'description' => __( 'The street number associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['street_number'] ) ? $root['street_number'] : null; }, - ], - 'city' => [ + ], + 'city' => [ 'type' => 'String', 'description' => __( 'The city associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['city'] ) ? $root['city'] : null; }, - ], - 'state' => [ + ], + 'state' => [ 'type' => 'String', 'description' => __( 'The state associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['state'] ) ? $root['state'] : null; }, - ], - 'stateShort' => [ + ], + 'stateShort' => [ 'type' => 'String', 'description' => __( 'The state abbreviation associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['state_short'] ) ? $root['state_short'] : null; }, - ], - 'postCode' => [ + ], + 'postCode' => [ 'type' => 'String', 'description' => __( 'The post code associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['post_code'] ) ? $root['post_code'] : null; }, - ], - 'country' => [ + ], + 'country' => [ 'type' => 'String', 'description' => __( 'The country associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['country'] ) ? $root['country'] : null; }, - ], - 'countryShort' => [ + ], + 'countryShort' => [ 'type' => 'String', 'description' => __( 'The country abbreviation associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['country_short'] ) ? $root['country_short'] : null; }, - ], - 'placeId' => [ + ], + 'placeId' => [ 'type' => 'String', 'description' => __( 'The country associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['place_id'] ) ? $root['place_id'] : null; }, - ], - 'zoom' => [ + ], + 'zoom' => [ 'type' => 'String', 'description' => __( 'The zoom defined with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['zoom'] ) ? $root['zoom'] : null; }, - ], - ]; - } + ], + ]; + } $this->type_registry->register_object_type( $field_type_name, @@ -1506,12 +1504,12 @@ protected function add_acf_fields_to_graphql_types() { } public function register_choices_of_acf_fields_as_enum_type( array $acf_field ): string { - // If the field isn't a select or radio field, return an empty string. - if ( 'select' !== $acf_field['type'] && 'radio' !== $acf_field['type'] ) { - return ''; + // If the field isn't a select or radio field or if there are no choices available, return 'String'. + if ( ( 'select' !== $acf_field['type'] && 'radio' !== $acf_field['type'] ) || empty( $acf_field['choices'] ) ) { + return 'String'; } - // Generate a unique name for the enum type using the field key. + // Generate a unique name for the enum type using the field name. $enum_type_name = ucfirst( self::camel_case( $acf_field['name'] ) ) . 'Enum'; if ( ! $this->type_registry->has_type( $enum_type_name ) ) { // Initialize an empty array to hold your enum values. diff --git a/src/class-mutations.php b/src/class-mutations.php index 9548209..7247bd1 100644 --- a/src/class-mutations.php +++ b/src/class-mutations.php @@ -318,11 +318,9 @@ private function register_graphql_field( array $config, string $parent_type_name case 'textarea': $field_config['type'] = 'String'; break; - case 'radio': - $enum_type = $this->config->register_choices_of_acf_fields_as_enum_type( $acf_field ); - $enum_type = ! empty( $enum_type ) ? $enum_type : 'String'; - $field_config['type'] = $enum_type; + $field_type = $this->config->register_choices_of_acf_fields_as_enum_type( $acf_field ); + $field_config['type'] = $field_type; break; case 'select': @@ -331,12 +329,11 @@ private function register_graphql_field( array $config, string $parent_type_name * the field will accept a string, but if it is configured to allow * multiple values it will accept a list of strings */ - $enum_type = $this->config->register_choices_of_acf_fields_as_enum_type( $acf_field ); - $enum_type = ! empty( $enum_type ) ? $enum_type : 'String'; + $field_type = $this->config->register_choices_of_acf_fields_as_enum_type( $acf_field ); if ( empty( $acf_field['multiple'] ) ) { - $field_config['type'] = $enum_type; + $field_config['type'] = $field_type; } else { - $field_config['type'] = [ 'list_of' => $enum_type ]; + $field_config['type'] = [ 'list_of' => $field_type ]; } break; case 'number': From 0e5b454f237eda2c7c2e52b4f29bfa93c9fdc9cb Mon Sep 17 00:00:00 2001 From: mohjak Date: Thu, 27 Jul 2023 11:56:59 +0300 Subject: [PATCH 7/9] fix: revert spaces --- src/class-config.php | 46 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/class-config.php b/src/class-config.php index 8ca5a0b..99de5ba 100644 --- a/src/class-config.php +++ b/src/class-config.php @@ -988,79 +988,79 @@ protected function register_graphql_field( string $type_name, string $field_name // ACF 5.8.6 added more data to Google Maps field value // https://www.advancedcustomfields.com/changelog/ if ( \acf_version_compare(acf_get_db_version(), '>=', '5.8.6' ) ) { - $fields += [ - 'streetName' => [ + $fields += [ + 'streetName' => [ 'type' => 'String', 'description' => __( 'The street name associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['street_name'] ) ? $root['street_name'] : null; }, - ], - 'streetNumber' => [ + ], + 'streetNumber' => [ 'type' => 'String', 'description' => __( 'The street number associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['street_number'] ) ? $root['street_number'] : null; }, - ], - 'city' => [ + ], + 'city' => [ 'type' => 'String', 'description' => __( 'The city associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['city'] ) ? $root['city'] : null; }, - ], - 'state' => [ + ], + 'state' => [ 'type' => 'String', 'description' => __( 'The state associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['state'] ) ? $root['state'] : null; }, - ], - 'stateShort' => [ + ], + 'stateShort' => [ 'type' => 'String', 'description' => __( 'The state abbreviation associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['state_short'] ) ? $root['state_short'] : null; }, - ], - 'postCode' => [ + ], + 'postCode' => [ 'type' => 'String', 'description' => __( 'The post code associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['post_code'] ) ? $root['post_code'] : null; }, - ], - 'country' => [ + ], + 'country' => [ 'type' => 'String', 'description' => __( 'The country associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['country'] ) ? $root['country'] : null; }, - ], - 'countryShort' => [ + ], + 'countryShort' => [ 'type' => 'String', 'description' => __( 'The country abbreviation associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['country_short'] ) ? $root['country_short'] : null; }, - ], - 'placeId' => [ + ], + 'placeId' => [ 'type' => 'String', 'description' => __( 'The country associated with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['place_id'] ) ? $root['place_id'] : null; }, - ], - 'zoom' => [ + ], + 'zoom' => [ 'type' => 'String', 'description' => __( 'The zoom defined with the map', 'wp-graphql-acf' ), 'resolve' => function( $root ) { return isset( $root['zoom'] ) ? $root['zoom'] : null; }, - ], - ]; - } + ], + ]; + } $this->type_registry->register_object_type( $field_type_name, From e2cb049f6860e6b959fd84876340ca8ec1b2b909 Mon Sep 17 00:00:00 2001 From: Alaa Date: Fri, 15 Dec 2023 10:42:54 +0300 Subject: [PATCH 8/9] fix: Use same hook that acf normally used to save its data when create/update post/term from UI and that to be more compatible with other plugins that waiting the acf to be saved to do something depending on it like WPML sync feature that sync acf data from master post to its translations. Also fix a case that relationships field can be single id so wrap it in array --- src/class-config.php | 6 +++++- src/class-mutations.php | 15 +++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/class-config.php b/src/class-config.php index 99de5ba..41b7135 100644 --- a/src/class-config.php +++ b/src/class-config.php @@ -692,7 +692,11 @@ protected function register_graphql_field( string $type_name, string $field_name $relationship = []; $value = $this->get_acf_field_value( $root, $acf_field ); - if ( ! empty( $value ) && is_array( $value ) ) { + if ( ! empty( $value ) ) { + // It sometimes saved as single id like in case of WPML sync acf field to translations posts + if ( ! is_array( $value ) ) { + $value = [ $value ]; + } foreach ( $value as $post_id ) { $post_object = get_post( $post_id ); if ( $post_object instanceof \WP_Post ) { diff --git a/src/class-mutations.php b/src/class-mutations.php index 7247bd1..ef75438 100644 --- a/src/class-mutations.php +++ b/src/class-mutations.php @@ -70,15 +70,22 @@ public function init( TypeRegistry $type_registry, Config $config ): void */ $this->add_acf_fields_to_graphql_types(); - add_action( 'graphql_post_object_mutation_update_additional_data', function ( int $post_id, array $input ) { - $this->save_registered_fields_data( $post_id, self::POST_OBJECT_TYPE, $input, $this->registered_fields ); + // Use same hook that acf normally used to save its data when post create/update from UI and that to be + // more compatible with other plugins that waiting the acf to save to do something depending on it like + // WPML sync feature that sync acf data from master post to its translations. + add_filter( 'graphql_post_object_insert_post_args', function ( array $insert_post_args, array $input ) { + self::add_action_once( 'save_post', function ( int $post_id ) use ( $input ) { + $this->save_registered_fields_data( $post_id, self::POST_OBJECT_TYPE, $input, $this->registered_fields ); + } ); + + return $insert_post_args; }, 10, 2 ); add_filter( 'graphql_term_object_insert_term_args', function ( array $insert_args, array $input ) { - self::add_action_once( 'graphql_insert_term', function ( int $term_id ) use ( $input ) { + self::add_action_once( 'create_term', function ( int $term_id ) use ( $input ) { $this->save_registered_fields_data( $term_id, self::TERM_OBJECT_TYPE, $input, $this->registered_fields ); } ); - self::add_action_once( 'graphql_update_term', function ( int $term_id ) use ( $input ) { + self::add_action_once( 'edit_term', function ( int $term_id ) use ( $input ) { $this->save_registered_fields_data( $term_id, self::TERM_OBJECT_TYPE, $input, $this->registered_fields ); } ); From 0a48c0b487c1023f7d29f7a041c6373c0301388c Mon Sep 17 00:00:00 2001 From: Alaa Date: Thu, 1 Feb 2024 14:41:42 +0300 Subject: [PATCH 9/9] fix: ignoring revision type because the ACF data was saving in the revision instead of the real post that we need --- src/class-mutations.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/class-mutations.php b/src/class-mutations.php index ef75438..be0a2d3 100644 --- a/src/class-mutations.php +++ b/src/class-mutations.php @@ -2,6 +2,7 @@ namespace WPGraphQL\ACF; +use WP_Post; use WPGraphQL\Registry\TypeRegistry; use WPGraphQL\Utils\Utils; @@ -74,9 +75,15 @@ public function init( TypeRegistry $type_registry, Config $config ): void // more compatible with other plugins that waiting the acf to save to do something depending on it like // WPML sync feature that sync acf data from master post to its translations. add_filter( 'graphql_post_object_insert_post_args', function ( array $insert_post_args, array $input ) { - self::add_action_once( 'save_post', function ( int $post_id ) use ( $input ) { + self::add_action_once( 'save_post', function ( int $post_id, WP_Post $post ) use ( $input ) { + // Ignore revision because it's not the post that updated and we don't want to run saving ACF data into it. + if ( 'revision' === $post->post_type ) { + // Return false to prevent removing action because it's not the action that we need. + return false; + } + $this->save_registered_fields_data( $post_id, self::POST_OBJECT_TYPE, $input, $this->registered_fields ); - } ); + }, 10, 2 ); return $insert_post_args; }, 10, 2 ); @@ -577,8 +584,10 @@ private function add_acf_fields_to_graphql_types() { */ public static function add_action_once( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): bool { $singular = function () use ( $hook_name, $callback, $priority, $accepted_args, &$singular ) { - remove_action( $hook_name, $singular, $priority ); - call_user_func_array( $callback, func_get_args() ); + $should_be_removed = call_user_func_array( $callback, func_get_args() ); + if ( false !== $should_be_removed ) { + remove_action( $hook_name, $singular, $priority ); + } }; return add_action( $hook_name, $singular, $priority, $accepted_args );