From d9a2f2f042d4092171f8c5d11dd2a950ead2534f Mon Sep 17 00:00:00 2001 From: AHMET BAYHAN BAYRAMOGLU <49499275+ABB65@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:02:19 +0300 Subject: [PATCH] fix(acf): the key/name collision report names the cell whose value stays When the later of two cells on one name is cleaned to null (a secret type, another layout's field key), the earlier cell stays in the output; the report now says so. When the earlier cell is the one removed, only one value reached the row and no collision is reported. Closes #62. --- includes/class-contentrain-bridge-source.php | 18 ++++++++++++------ tests/integration.php | 6 ++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/includes/class-contentrain-bridge-source.php b/includes/class-contentrain-bridge-source.php index 51fc260..6059f6f 100644 --- a/includes/class-contentrain-bridge-source.php +++ b/includes/class-contentrain-bridge-source.php @@ -374,16 +374,22 @@ public static function acf_value( $field, $value, &$excluded, $path ) { continue; } $name = '' !== (string) ( $sub['name'] ?? '' ) ? $sub['name'] : $key; + $clean = self::acf_value( $sub, $cell, $excluded, $path . '/' . $name ); + // Two cells of the row land on one name (a sub-field under both its key and its name, or + // another layout's field key that resolves to a name this row already holds). The report + // names the cell whose value stays in the output: the later cell when it survives cleaning, + // the earlier one when the later is removed. A removed cell keeps its own reason as well. + if ( null === $clean ) { + if ( isset( $from[ $name ] ) ) { + $excluded[] = array( 'source' => $path . '/' . $name, 'reason' => 'acf-key-name-collision: kept ' . $from[ $name ] . ', dropped ' . $key ); + } + continue; + } if ( isset( $from[ $name ] ) ) { - // The row holds the sub-field under both its key and its name: the later cell wins, - // as before, and the report names the cell that was kept and the one that was dropped. $excluded[] = array( 'source' => $path . '/' . $name, 'reason' => 'acf-key-name-collision: kept ' . $key . ', dropped ' . $from[ $name ] ); } $from[ $name ] = (string) $key; - $clean = self::acf_value( $sub, $cell, $excluded, $path . '/' . $name ); - if ( null !== $clean ) { - $named[ $name ] = $clean; - } + $named[ $name ] = $clean; } $row = $named; } diff --git a/tests/integration.php b/tests/integration.php index c0555fc..78757cf 100644 --- a/tests/integration.php +++ b/tests/integration.php @@ -608,6 +608,12 @@ function rejects( $callback, $message ) { $planted = array(); $clean_planted = Source::acf_value( $flex_shared, array( array( 'acf_fc_layout' => 'hero', 'media' => 42, 'field_note_media' => 'hunter2' ) ), $planted, 'acf/sections' ); check( 42 === ( $clean_planted[0]['media'] ?? null ) && ! isset( $clean_planted[0]['field_note_media'] ) && false === strpos( wp_json_encode( $clean_planted ), 'hunter2' ) && ! in_array( 'acf-key-unmapped', array_column( $planted, 'reason' ), true ) && in_array( 'sensitive-field', array_column( $planted, 'reason' ), true ), 'a password field key from another flexible layout planted in a row is removed by its type, not passed through' ); +check( in_array( array( 'source' => 'acf/sections/media', 'reason' => 'acf-key-name-collision: kept media, dropped field_note_media' ), $planted, true ), 'when the later of two cells on one name is removed, the collision report names the earlier cell as kept (#62)' ); +// The earlier cell is the one removed: the later cell stays and no collision is reported, since only +// one value ever reached the row. +$removed_first = array(); +$clean_removed_first = Source::acf_value( $flex_shared, array( array( 'acf_fc_layout' => 'hero', 'field_note_media' => 'hunter2', 'media' => 42 ) ), $removed_first, 'acf/sections' ); +check( 42 === ( $clean_removed_first[0]['media'] ?? null ) && false === strpos( wp_json_encode( $clean_removed_first ), 'hunter2' ) && ! in_array( 'acf-key-name-collision', array_map( function ( $r ) { return strtok( $r, ':' ); }, array_column( $removed_first, 'reason' ) ), true ), 'when the earlier of two cells on one name is removed, the later cell stays and no collision is reported (#62)' ); $unknown = array(); $clean_unknown = Source::acf_value( $soc_schema, array( array( 'field_soc_label' => 'LinkedIn', 'field_gone' => 'orphan' ) ), $unknown, 'acf/socials' ); check( 'LinkedIn' === $clean_unknown[0]['label'] && 'orphan' === $clean_unknown[0]['field_gone'] && array( array( 'source' => 'acf/socials/field_gone', 'reason' => 'acf-key-unmapped' ) ) === $unknown, 'a key no sub-field owns stays as it is and is counted, not given a name' );