From c8cff833b4f1d92167e88ba67032fdbaceed20cf Mon Sep 17 00:00:00 2001 From: tomasznowakx Date: Tue, 24 Mar 2026 13:43:05 +0100 Subject: [PATCH 1/3] Implements ACF fields support for blocks --- docs/BLOCKS_ACF.md | 400 +++++++++++++++++++++++++++++++++ docs/README.md | 2 + inc/Abilities/BlockAdd.php | 38 +++- inc/Abilities/BlockReplace.php | 39 +++- inc/Abilities/BlockUpdate.php | 34 ++- inc/Helpers/AcfHelper.php | 94 ++++++++ 6 files changed, 600 insertions(+), 7 deletions(-) create mode 100644 docs/BLOCKS_ACF.md create mode 100644 inc/Helpers/AcfHelper.php diff --git a/docs/BLOCKS_ACF.md b/docs/BLOCKS_ACF.md new file mode 100644 index 0000000..06e4d62 --- /dev/null +++ b/docs/BLOCKS_ACF.md @@ -0,0 +1,400 @@ +# ACF Block Fields Support + +## Overview + +The `BlockAdd`, `BlockUpdate`, and `BlockReplace` abilities support an optional `acf_fields` parameter that allows you to populate or update Advanced Custom Fields (ACF) field values attached to an ACF-registered Gutenberg block. + +## Prerequisites + +- The [Advanced Custom Fields](https://www.advancedcustomfields.com/) plugin (free or PRO) must be installed and active. +- The target block must be registered via `acf_register_block_type()`. +- You must know the ACF field names assigned to the block's field group. + +## How It Works + +ACF stores block field values in the database using a synthetic post ID of the form `block_{block_id}`, where `block_id` is a unique identifier stored in the block's `id` attribute (e.g. `block_682a1c3f4b2e8`). + +When you provide `acf_fields`: + +1. A unique block `id` is generated automatically (if the block does not already have one) and saved as a block attribute. +2. The block is written to the post content. +3. `update_field()` is called for each key/value pair in `acf_fields`, targeting the block's `id`. + +On subsequent **updates**, the existing `id` is read from the block attrs and reused so that previously saved field values are preserved and overwritten correctly. + +## Parameter + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `acf_fields` | object | No | Key/value map of ACF field names to their values | + +## Error Codes + +| Code | Description | +|------|-------------| +| `acf_not_active` | ACF plugin is not installed or active | +| `acf_update_failed` | `update_field()` returned `false` for a specific field | + +--- + +## Examples by Field Type + +### Text / Textarea / Number / Email / URL / Password / Range + +Pass the raw scalar value. + +```json +{ + "post_id": 42, + "block_index": 0, + "acf_fields": { + "heading": "Welcome to our site", + "intro_text": "This is a longer description.", + "rating": 5, + "website": "https://example.com" + } +} +``` + +--- + +### True / False (Checkbox toggle) + +Pass a boolean. + +```json +{ + "acf_fields": { + "is_featured": true, + "show_button": false + } +} +``` + +--- + +### Select / Radio Button / Button Group + +Pass the choice key (string) as defined in the field settings. + +```json +{ + "acf_fields": { + "layout_style": "wide", + "color_scheme": "dark" + } +} +``` + +For **multi-select** (Select with `multiple` enabled), pass an array of keys: + +```json +{ + "acf_fields": { + "tags": ["news", "featured", "homepage"] + } +} +``` + +--- + +### Checkbox + +Pass an array of selected choice keys. + +```json +{ + "acf_fields": { + "features": ["wifi", "parking", "pool"] + } +} +``` + +--- + +### Image / File + +Pass the WordPress attachment ID (integer). + +```json +{ + "acf_fields": { + "hero_image": 123, + "brochure_pdf": 456 + } +} +``` + +--- + +### Gallery + +Pass an array of attachment IDs. + +```json +{ + "acf_fields": { + "photo_gallery": [101, 102, 103, 104] + } +} +``` + +--- + +### Link + +Pass an object with `url`, `title`, and `target` keys. + +```json +{ + "acf_fields": { + "cta_link": { + "url": "https://example.com/contact", + "title": "Contact Us", + "target": "_blank" + } + } +} +``` + +--- + +### Post Object / Relationship + +Pass a single post ID (integer) for Post Object, or an array of post IDs for Relationship. + +```json +{ + "acf_fields": { + "related_post": 99, + "related_articles": [10, 20, 30] + } +} +``` + +--- + +### Taxonomy + +Pass a single term ID (integer) for single-select, or an array for multi-select. + +```json +{ + "acf_fields": { + "primary_category": 7, + "tags": [7, 12, 15] + } +} +``` + +--- + +### User + +Pass a single user ID (integer) for single-select, or an array for multi-select. + +```json +{ + "acf_fields": { + "author": 3, + "team_members": [3, 8, 14] + } +} +``` + +--- + +### Date Picker / Date Time Picker / Time Picker + +Pass a string in the format configured in the field settings (default: `Y-m-d` for date, `Y-m-d H:i:s` for date-time, `H:i:s` for time). + +```json +{ + "acf_fields": { + "event_date": "2025-06-15", + "event_start_time": "09:00:00", + "published_at": "2025-06-15 09:00:00" + } +} +``` + +--- + +### Color Picker + +Pass a hex color string. + +```json +{ + "acf_fields": { + "background_color": "#ff6600", + "text_color": "#ffffff" + } +} +``` + +--- + +### Google Map + +Pass an object with `address`, `lat`, and `lng` keys. + +```json +{ + "acf_fields": { + "office_location": { + "address": "1 Infinite Loop, Cupertino, CA 95014", + "lat": 37.3318, + "lng": -122.0312 + } + } +} +``` + +--- + +### oEmbed + +Pass the raw URL to embed. + +```json +{ + "acf_fields": { + "promo_video": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + } +} +``` + +--- + +### Group + +Pass a nested object matching the group's sub-field names. + +```json +{ + "acf_fields": { + "hero_content": { + "title": "Big Headline", + "subtitle": "Supporting copy", + "image": 77 + } + } +} +``` + +--- + +### Repeater + +Pass an array of row objects, where each object contains the repeater's sub-field names. + +```json +{ + "acf_fields": { + "team_members": [ + { "name": "Alice", "role": "Lead Developer", "photo": 201 }, + { "name": "Bob", "role": "Designer", "photo": 202 } + ] + } +} +``` + +--- + +### Flexible Content + +Pass an array of layout objects. Each object must have an `acf_fc_layout` key matching the layout name, plus the layout's sub-field values. + +```json +{ + "acf_fields": { + "page_sections": [ + { + "acf_fc_layout": "hero", + "heading": "Welcome", + "image": 50 + }, + { + "acf_fc_layout": "text_block", + "content": "This is a paragraph of body copy." + }, + { + "acf_fc_layout": "cta_banner", + "button_label": "Learn More", + "button_url": "https://example.com" + } + ] + } +} +``` + +--- + +## Complete Workflow Example + +### Add an ACF hero block and populate its fields + +**Step 1 – Find or create the post** + +```json +{ + "post_title": "My Landing Page" +} +``` + +Tool: `xfive-posts/post-by-title` + +**Step 2 – Add the ACF block with fields** + +```json +{ + "post_id": 42, + "block": "acf/hero", + "acf_fields": { + "heading": "Transform Your Business", + "subheading": "We help companies grow faster.", + "background_image": 123, + "cta_link": { + "url": "/contact", + "title": "Get Started" + }, + "show_overlay": true + } +} +``` + +Tool: `xfive-blocks/block-add` + +Response includes `block_id` confirming the field association: + +```json +{ + "added": true, + "block_name": "acf/hero", + "block_id": "block_682a1c3f4b2e8" +} +``` + +**Step 3 – Update just the heading later** + +```json +{ + "post_id": 42, + "block_index": 0, + "acf_fields": { + "heading": "Accelerate Your Growth" + } +} +``` + +Tool: `xfive-blocks/block-update` + +--- + +## Notes + +- `acf_fields` is **optional**. If omitted, the ability behaves exactly as before — no ACF operations are performed. +- You do **not** need to pass `acf_fields` for standard WordPress core blocks; it is only meaningful for ACF-registered blocks. +- Field values are saved using ACF's own `update_field()` function, so all ACF hooks, formatting, and validation rules apply normally. +- For **nested fields** inside repeaters or flexible content, pass the full nested structure in a single call rather than using ACF's `add_row()` / `update_row()` API — `update_field()` with a complete array value handles this correctly. diff --git a/docs/README.md b/docs/README.md index 1b4e1bf..7870e80 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ This plugin exposes WordPress content management capabilities through MCP (Model - **[Blocks - Remove](BLOCKS_REMOVE.md)** - Delete blocks from posts - **[Blocks - Move](BLOCKS_MOVE.md)** - Reorder blocks within a post - **[Blocks - Tree](BLOCKS_TREE.md)** - View the block structure of a post +- **[Blocks - ACF Fields](BLOCKS_ACF.md)** - Populate ACF custom fields on ACF-registered blocks ### Post Management @@ -305,6 +306,7 @@ Common errors: - [Blocks - Remove](BLOCKS_REMOVE.md) - [Blocks - Move](BLOCKS_MOVE.md) - [Blocks - Tree](BLOCKS_TREE.md) +- [Blocks - ACF Fields](BLOCKS_ACF.md) ### Post Abilities diff --git a/inc/Abilities/BlockAdd.php b/inc/Abilities/BlockAdd.php index b539c08..7a5bda5 100644 --- a/inc/Abilities/BlockAdd.php +++ b/inc/Abilities/BlockAdd.php @@ -3,6 +3,7 @@ namespace XfiveMCP\Abilities; use XfiveMCP\Blocks\BlockRegistry; +use XfiveMCP\Helpers\AcfHelper; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. @@ -72,6 +73,11 @@ public function get_input_schema(): array { 'additionalProperties' => true, ), ), + 'acf_fields' => array( + 'type' => 'object', + 'description' => 'ACF custom field values to save for the block. Keys are ACF field names, values are the data to store. Supports all ACF field types (text, image, repeater, group, etc.).', + 'additionalProperties' => true, + ), ), 'required' => array( 'post_id', 'block' ), ); @@ -101,13 +107,23 @@ public function get_output_schema(): array { * @return array|\WP_Error Result array or error. */ public function execute_callback( array $args = array() ) { - $post_id = absint( $args['post_id'] ); - $post = get_post( $post_id ); + $post_id = absint( $args['post_id'] ); + $acf_fields = $args['acf_fields'] ?? array(); + $post = get_post( $post_id ); if ( ! $post ) { return new \WP_Error( 'post_not_found', 'Post not found' ); } + if ( ! empty( $acf_fields ) && ! AcfHelper::is_acf_active() ) { + return new \WP_Error( 'acf_not_active', 'Advanced Custom Fields plugin is not active.' ); + } + + // Generate a block ID so ACF can associate field values with this block. + if ( ! empty( $acf_fields ) && empty( $args['attributes']['id'] ) ) { + $args['attributes']['id'] = AcfHelper::generate_block_id(); + } + $blocks = parse_blocks( $post->post_content ); $new_block = BlockRegistry::get_instance()->normalize_block( $args ); @@ -128,9 +144,25 @@ public function execute_callback( array $args = array() ) { return $result; } - return array( + $block_id = AcfHelper::extract_block_id( $new_block ); + + if ( ! empty( $acf_fields ) && ! empty( $block_id ) ) { + $acf_result = AcfHelper::update_block_fields( $block_id, $acf_fields ); + + if ( is_wp_error( $acf_result ) ) { + return $acf_result; + } + } + + $response = array( 'added' => true, 'block_name' => $args['block'] ?? $args['blockName'] ?? '', ); + + if ( ! empty( $block_id ) ) { + $response['block_id'] = $block_id; + } + + return $response; } } diff --git a/inc/Abilities/BlockReplace.php b/inc/Abilities/BlockReplace.php index cd1f8aa..89a801c 100644 --- a/inc/Abilities/BlockReplace.php +++ b/inc/Abilities/BlockReplace.php @@ -3,6 +3,7 @@ namespace XfiveMCP\Abilities; use XfiveMCP\Blocks\BlockRegistry; +use XfiveMCP\Helpers\AcfHelper; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. @@ -77,6 +78,11 @@ public function get_input_schema(): array { 'additionalProperties' => true, ), ), + 'acf_fields' => array( + 'type' => 'object', + 'description' => 'ACF custom field values to save for the replacement block. Keys are ACF field names, values are the data to store. Supports all ACF field types (text, image, repeater, group, etc.).', + 'additionalProperties' => true, + ), ), 'required' => array( 'post_id', 'block_index', 'block' ), ); @@ -116,12 +122,17 @@ public function get_output_schema(): array { public function execute_callback( array $args = array() ): array|object { $post_id = absint( $args['post_id'] ); $block_index = absint( $args['block_index'] ); + $acf_fields = $args['acf_fields'] ?? array(); $post = get_post( $post_id ); if ( ! $post ) { return new \WP_Error( 'post_not_found', 'Post not found' ); } + if ( ! empty( $acf_fields ) && ! AcfHelper::is_acf_active() ) { + return new \WP_Error( 'acf_not_active', 'Advanced Custom Fields plugin is not active.' ); + } + $blocks = parse_blocks( $post->post_content ); if ( ! isset( $blocks[ $block_index ] ) ) { @@ -134,10 +145,16 @@ public function execute_callback( array $args = array() ): array|object { // Get the old block name. $old_block_name = $blocks[ $block_index ]['blockName'] ?? 'unknown'; - // Build the new block data. + // Build the new block data, generating a fresh ID for ACF association. + $new_attrs = $args['attributes'] ?? array(); + + if ( ! empty( $acf_fields ) && empty( $new_attrs['id'] ) ) { + $new_attrs['id'] = AcfHelper::generate_block_id(); + } + $new_block_data = array( 'block' => $args['block'], - 'attributes' => $args['attributes'] ?? array(), + 'attributes' => $new_attrs, 'innerBlocks' => $args['innerBlocks'] ?? array(), ); @@ -161,10 +178,26 @@ public function execute_callback( array $args = array() ): array|object { return $result; } - return array( + $block_id = AcfHelper::extract_block_id( $new_block ); + + if ( ! empty( $acf_fields ) && ! empty( $block_id ) ) { + $acf_result = AcfHelper::update_block_fields( $block_id, $acf_fields ); + + if ( is_wp_error( $acf_result ) ) { + return $acf_result; + } + } + + $response = array( 'replaced' => true, 'old_block_name' => $old_block_name, 'new_block_name' => $args['block'], ); + + if ( ! empty( $block_id ) ) { + $response['block_id'] = $block_id; + } + + return $response; } } diff --git a/inc/Abilities/BlockUpdate.php b/inc/Abilities/BlockUpdate.php index ffa08fc..b11aa93 100644 --- a/inc/Abilities/BlockUpdate.php +++ b/inc/Abilities/BlockUpdate.php @@ -3,6 +3,7 @@ namespace XfiveMCP\Abilities; use XfiveMCP\Blocks\BlockRegistry; +use XfiveMCP\Helpers\AcfHelper; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. @@ -71,6 +72,11 @@ public function get_input_schema(): array { 'additionalProperties' => true, ), ), + 'acf_fields' => array( + 'type' => 'object', + 'description' => 'ACF custom field values to update for the block. Keys are ACF field names, values are the data to store. Supports all ACF field types (text, image, repeater, group, etc.).', + 'additionalProperties' => true, + ), ), 'required' => array( 'post_id', 'block_index' ), ); @@ -117,12 +123,17 @@ public function get_output_schema(): array { public function execute_callback( array $args = array() ): array|object { $post_id = absint( $args['post_id'] ); $block_index = absint( $args['block_index'] ); + $acf_fields = $args['acf_fields'] ?? array(); $post = get_post( $post_id ); if ( ! $post ) { return new \WP_Error( 'post_not_found', 'Post not found' ); } + if ( ! empty( $acf_fields ) && ! AcfHelper::is_acf_active() ) { + return new \WP_Error( 'acf_not_active', 'Advanced Custom Fields plugin is not active.' ); + } + $blocks = parse_blocks( $post->post_content ); if ( ! isset( $blocks[ $block_index ] ) ) { @@ -158,6 +169,11 @@ public function execute_callback( array $args = array() ): array|object { $args['attributes'] ?? array() ); + // Ensure the block has an ID for ACF field association. + if ( ! empty( $acf_fields ) && empty( $new_attrs['id'] ) ) { + $new_attrs['id'] = AcfHelper::generate_block_id(); + } + // Handle inner blocks: if provided, replace; otherwise keep existing. $new_inner_blocks_data = $args['innerBlocks'] ?? $existing_block['innerBlocks'] ?? array(); @@ -187,9 +203,25 @@ public function execute_callback( array $args = array() ): array|object { return $result; } - return array( + $block_id = AcfHelper::extract_block_id( $updated_block ); + + if ( ! empty( $acf_fields ) && ! empty( $block_id ) ) { + $acf_result = AcfHelper::update_block_fields( $block_id, $acf_fields ); + + if ( is_wp_error( $acf_result ) ) { + return $acf_result; + } + } + + $response = array( 'updated' => true, 'block_name' => $new_block_name, ); + + if ( ! empty( $block_id ) ) { + $response['block_id'] = $block_id; + } + + return $response; } } diff --git a/inc/Helpers/AcfHelper.php b/inc/Helpers/AcfHelper.php new file mode 100644 index 0000000..237dce0 --- /dev/null +++ b/inc/Helpers/AcfHelper.php @@ -0,0 +1,94 @@ + value. + * @return true|\WP_Error True on success, WP_Error on the first failure. + */ + public static function update_block_fields( string $block_id, array $acf_fields ): bool|\WP_Error { + $acf_post_id = self::get_block_acf_post_id( $block_id ); + + foreach ( $acf_fields as $field_name => $value ) { + $result = update_field( $field_name, $value, $acf_post_id ); + + if ( false === $result ) { + return new \WP_Error( + 'acf_update_failed', + sprintf( + 'Failed to update ACF field "%s" for block "%s".', + $field_name, + $block_id + ) + ); + } + } + + return true; + } + + /** + * Extract the block client ID from a normalised WP block array. + * + * Reads attrs['id'] from the block data as serialised by serialize_blocks(). + * + * @param array $block Normalised block array (WordPress block format). + * @return string Block ID, or empty string if not set. + */ + public static function extract_block_id( array $block ): string { + return $block['attrs']['id'] ?? ''; + } +} From 561997a6111824c84d53f733bb1c43d2eb8450a8 Mon Sep 17 00:00:00 2001 From: tomasznowakx Date: Tue, 24 Mar 2026 14:21:47 +0100 Subject: [PATCH 2/3] Fix approach from old to new one based on HTML attrs --- docs/BLOCKS_ACF.md | 14 +++-- inc/Abilities/BlockAdd.php | 22 ++++---- inc/Abilities/BlockReplace.php | 23 ++++---- inc/Abilities/BlockUpdate.php | 22 ++++---- inc/Helpers/AcfHelper.php | 97 +++++++++++++++++++--------------- 5 files changed, 90 insertions(+), 88 deletions(-) diff --git a/docs/BLOCKS_ACF.md b/docs/BLOCKS_ACF.md index 06e4d62..0a89bb1 100644 --- a/docs/BLOCKS_ACF.md +++ b/docs/BLOCKS_ACF.md @@ -12,15 +12,19 @@ The `BlockAdd`, `BlockUpdate`, and `BlockReplace` abilities support an optional ## How It Works -ACF stores block field values in the database using a synthetic post ID of the form `block_{block_id}`, where `block_id` is a unique identifier stored in the block's `id` attribute (e.g. `block_682a1c3f4b2e8`). +ACF blocks store their field values **directly inside the serialised block comment** in the `data` attribute, alongside ACF field key references: + +``` + +``` When you provide `acf_fields`: -1. A unique block `id` is generated automatically (if the block does not already have one) and saved as a block attribute. -2. The block is written to the post content. -3. `update_field()` is called for each key/value pair in `acf_fields`, targeting the block's `id`. +1. A unique block `id` is generated automatically (if the block does not already have one) and stored as a block attribute. +2. For each field, `acf_get_field()` is called to resolve the ACF field key (e.g. `field_abc123`), which is stored as `_field_name` alongside the value. +3. The complete `data` object is merged into the block's `attrs` and serialised into the block comment in a single `wp_update_post()` call. -On subsequent **updates**, the existing `id` is read from the block attrs and reused so that previously saved field values are preserved and overwritten correctly. +On subsequent **updates**, the existing `id` and `data` entries are read from the stored block attrs and the new values are merged on top, preserving any fields not included in the current call. ## Parameter diff --git a/inc/Abilities/BlockAdd.php b/inc/Abilities/BlockAdd.php index 7a5bda5..25e521e 100644 --- a/inc/Abilities/BlockAdd.php +++ b/inc/Abilities/BlockAdd.php @@ -119,9 +119,14 @@ public function execute_callback( array $args = array() ) { return new \WP_Error( 'acf_not_active', 'Advanced Custom Fields plugin is not active.' ); } - // Generate a block ID so ACF can associate field values with this block. - if ( ! empty( $acf_fields ) && empty( $args['attributes']['id'] ) ) { - $args['attributes']['id'] = AcfHelper::generate_block_id(); + if ( ! empty( $acf_fields ) ) { + // Generate a block ID so ACF can identify this block instance. + if ( empty( $args['attributes']['id'] ) ) { + $args['attributes']['id'] = AcfHelper::generate_block_id(); + } + + // Inject ACF field values into attrs['data'] before serialisation. + $args['attributes'] = AcfHelper::merge_acf_data( $args['attributes'] ?? array(), $acf_fields ); } $blocks = parse_blocks( $post->post_content ); @@ -144,21 +149,12 @@ public function execute_callback( array $args = array() ) { return $result; } - $block_id = AcfHelper::extract_block_id( $new_block ); - - if ( ! empty( $acf_fields ) && ! empty( $block_id ) ) { - $acf_result = AcfHelper::update_block_fields( $block_id, $acf_fields ); - - if ( is_wp_error( $acf_result ) ) { - return $acf_result; - } - } - $response = array( 'added' => true, 'block_name' => $args['block'] ?? $args['blockName'] ?? '', ); + $block_id = AcfHelper::extract_block_id( $new_block ); if ( ! empty( $block_id ) ) { $response['block_id'] = $block_id; } diff --git a/inc/Abilities/BlockReplace.php b/inc/Abilities/BlockReplace.php index 89a801c..ba14c10 100644 --- a/inc/Abilities/BlockReplace.php +++ b/inc/Abilities/BlockReplace.php @@ -145,11 +145,17 @@ public function execute_callback( array $args = array() ): array|object { // Get the old block name. $old_block_name = $blocks[ $block_index ]['blockName'] ?? 'unknown'; - // Build the new block data, generating a fresh ID for ACF association. + // Build the new block data. $new_attrs = $args['attributes'] ?? array(); - if ( ! empty( $acf_fields ) && empty( $new_attrs['id'] ) ) { - $new_attrs['id'] = AcfHelper::generate_block_id(); + if ( ! empty( $acf_fields ) ) { + // Generate a fresh block ID for the replacement block. + if ( empty( $new_attrs['id'] ) ) { + $new_attrs['id'] = AcfHelper::generate_block_id(); + } + + // Inject ACF field values into attrs['data'] before serialisation. + $new_attrs = AcfHelper::merge_acf_data( $new_attrs, $acf_fields ); } $new_block_data = array( @@ -178,22 +184,13 @@ public function execute_callback( array $args = array() ): array|object { return $result; } - $block_id = AcfHelper::extract_block_id( $new_block ); - - if ( ! empty( $acf_fields ) && ! empty( $block_id ) ) { - $acf_result = AcfHelper::update_block_fields( $block_id, $acf_fields ); - - if ( is_wp_error( $acf_result ) ) { - return $acf_result; - } - } - $response = array( 'replaced' => true, 'old_block_name' => $old_block_name, 'new_block_name' => $args['block'], ); + $block_id = AcfHelper::extract_block_id( $new_block ); if ( ! empty( $block_id ) ) { $response['block_id'] = $block_id; } diff --git a/inc/Abilities/BlockUpdate.php b/inc/Abilities/BlockUpdate.php index b11aa93..ba6ac53 100644 --- a/inc/Abilities/BlockUpdate.php +++ b/inc/Abilities/BlockUpdate.php @@ -169,9 +169,14 @@ public function execute_callback( array $args = array() ): array|object { $args['attributes'] ?? array() ); - // Ensure the block has an ID for ACF field association. - if ( ! empty( $acf_fields ) && empty( $new_attrs['id'] ) ) { - $new_attrs['id'] = AcfHelper::generate_block_id(); + if ( ! empty( $acf_fields ) ) { + // Ensure the block has an ID for ACF to identify this block instance. + if ( empty( $new_attrs['id'] ) ) { + $new_attrs['id'] = AcfHelper::generate_block_id(); + } + + // Inject ACF field values into attrs['data'] before serialisation. + $new_attrs = AcfHelper::merge_acf_data( $new_attrs, $acf_fields ); } // Handle inner blocks: if provided, replace; otherwise keep existing. @@ -203,21 +208,12 @@ public function execute_callback( array $args = array() ): array|object { return $result; } - $block_id = AcfHelper::extract_block_id( $updated_block ); - - if ( ! empty( $acf_fields ) && ! empty( $block_id ) ) { - $acf_result = AcfHelper::update_block_fields( $block_id, $acf_fields ); - - if ( is_wp_error( $acf_result ) ) { - return $acf_result; - } - } - $response = array( 'updated' => true, 'block_name' => $new_block_name, ); + $block_id = AcfHelper::extract_block_id( $updated_block ); if ( ! empty( $block_id ) ) { $response['block_id'] = $block_id; } diff --git a/inc/Helpers/AcfHelper.php b/inc/Helpers/AcfHelper.php index 237dce0..1fc2bd4 100644 --- a/inc/Helpers/AcfHelper.php +++ b/inc/Helpers/AcfHelper.php @@ -9,86 +9,95 @@ /** * Helper class for Advanced Custom Fields (ACF) integration. * - * Provides utility methods for reading and writing ACF field values - * associated with ACF-registered Gutenberg blocks. + * ACF blocks serialise their field values directly inside the block comment + * as an attrs['data'] object: + * + * + * + * This helper builds that data structure and injects it into block attrs + * before the block is serialised, so no separate postmeta write is needed. */ class AcfHelper { /** * Check whether ACF is active and available. * - * @return bool True if ACF's update_field() function exists. + * @return bool True if ACF's acf_get_field() function exists. */ public static function is_acf_active(): bool { - return function_exists( 'update_field' ); + return function_exists( 'acf_get_field' ); } /** - * Build the ACF post_id string for a given block client ID. + * Generate a unique block ID in the format ACF expects. * - * ACF stores block field values under a synthetic post_id of the - * form "block_{block_id}", where $block_id is the value stored in - * the block's attrs['id'] attribute. + * ACF block IDs look like "block_682a1c3f4b2e8" (the "block_" prefix is + * part of the ID itself, stored in attrs['id']). * - * @param string $block_id The block client ID (e.g. "block_abc123"). - * @return string ACF-compatible post_id string. + * @return string Unique block ID (e.g. "block_682a1c3f4b2e8"). */ - public static function get_block_acf_post_id( string $block_id ): string { - return 'block_' . $block_id; + public static function generate_block_id(): string { + return 'block_' . uniqid(); } /** - * Generate a unique block ID in the format ACF expects. + * Extract the block client ID from a normalised WP block array. * - * @return string Unique block ID (e.g. "block_682a1c3f4b2e8"). + * Reads attrs['id'] from the block data as serialised by serialize_blocks(). + * + * @param array $block Normalised block array (WordPress block format). + * @return string Block ID, or empty string if not set. */ - public static function generate_block_id(): string { - return 'block_' . uniqid(); + public static function extract_block_id( array $block ): string { + return $block['attrs']['id'] ?? ''; } /** - * Update ACF field values for a given block. + * Build the ACF data array for a block's attrs['data'] entry. * - * Iterates over the provided key→value map and calls update_field() - * for each entry using the block's ACF post_id. Complex types such as - * repeater rows, groups, flexible content layouts, galleries, and - * relationship arrays should be passed as nested arrays matching - * ACF's own update_field() format. + * ACF stores field values in the serialised block comment under a "data" + * key. Each field contributes two entries: + * - "field_name" => the raw value + * - "_field_name" => the ACF field key (e.g. "field_abc123") * - * @param string $block_id Block client ID (the value from attrs['id']). - * @param array $acf_fields Associative array of field_name => value. - * @return true|\WP_Error True on success, WP_Error on the first failure. + * If a field key cannot be resolved (field not registered), the entry is + * still written without the _field_name reference so the value is not lost. + * + * @param array $acf_fields Associative array of field_name => value. + * @return array Data array suitable for attrs['data']. */ - public static function update_block_fields( string $block_id, array $acf_fields ): bool|\WP_Error { - $acf_post_id = self::get_block_acf_post_id( $block_id ); + public static function build_block_data( array $acf_fields ): array { + $data = array(); foreach ( $acf_fields as $field_name => $value ) { - $result = update_field( $field_name, $value, $acf_post_id ); + $data[ $field_name ] = $value; - if ( false === $result ) { - return new \WP_Error( - 'acf_update_failed', - sprintf( - 'Failed to update ACF field "%s" for block "%s".', - $field_name, - $block_id - ) - ); + // Resolve the ACF field key for the _field_name reference. + $field_object = acf_get_field( $field_name ); + if ( $field_object && ! empty( $field_object['key'] ) ) { + $data[ '_' . $field_name ] = $field_object['key']; } } - return true; + return $data; } /** - * Extract the block client ID from a normalised WP block array. + * Merge ACF field data into a block attributes array. * - * Reads attrs['id'] from the block data as serialised by serialize_blocks(). + * Reads any existing attrs['data'], merges the new field values on top, + * and returns the full attrs array with the updated 'data' key. * - * @param array $block Normalised block array (WordPress block format). - * @return string Block ID, or empty string if not set. + * @param array $attrs Existing block attributes array. + * @param array $acf_fields Associative array of field_name => value. + * @return array Updated attributes array. */ - public static function extract_block_id( array $block ): string { - return $block['attrs']['id'] ?? ''; + public static function merge_acf_data( array $attrs, array $acf_fields ): array { + $existing_data = $attrs['data'] ?? array(); + $new_data = self::build_block_data( $acf_fields ); + + $attrs['data'] = array_merge( $existing_data, $new_data ); + + return $attrs; } } From c737153209638267a6256aff01c0bdb142fbab6c Mon Sep 17 00:00:00 2001 From: tomasznowakx Date: Wed, 25 Mar 2026 14:25:48 +0100 Subject: [PATCH 3/3] Increment version --- xfive-mcp.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xfive-mcp.php b/xfive-mcp.php index bea6086..51d5435 100644 --- a/xfive-mcp.php +++ b/xfive-mcp.php @@ -2,7 +2,7 @@ /** * Plugin Name: xfive Socrates - WordPress MCP Server with Abilities API * Description: MCP server with WordPress Abilities API - * Version: 1.2.1 + * Version: 1.2.2 * Author: Xfive * Author URI: https://xfive.co * Copyright: Xfive @@ -26,7 +26,7 @@ * @var string */ if ( ! defined( 'XFIVE_MCP_VERSION' ) ) { - define( 'XFIVE_MCP_VERSION', '1.2.1' ); + define( 'XFIVE_MCP_VERSION', '1.2.2' ); } /**