Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions includes/acf-field-group-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,148 @@ function acf_get_field_group( $id = 0 ) {
return acf_get_internal_post_type( $id, 'acf-field-group' );
}

/**
* Returns the post types targeted by a field group for PHP export.
*
* Only location groups made up of a single "post_type" rule using the "=="
* operator are returned. Anything else is treated as ambiguous and skipped.
*
* @since SCF 6.9.6
*
* @param array $field_group The field group array.
* @return array
*/
function acf_get_field_group_post_types_for_export( array $field_group ) {
if ( empty( $field_group['location'] ) || ! is_array( $field_group['location'] ) ) {
return array();
}

$post_types = array();

foreach ( $field_group['location'] as $rule_group ) {
if ( ! is_array( $rule_group ) || 1 !== count( $rule_group ) ) {
return array();
}

$rule = reset( $rule_group );
if ( ! is_array( $rule ) || 'post_type' !== ( $rule['param'] ?? '' ) || '==' !== ( $rule['operator'] ?? '' ) || empty( $rule['value'] ) || ! is_string( $rule['value'] ) ) {
return array();
}

$post_types[] = $rule['value'];
}

return array_values( array_unique( array_filter( $post_types, 'is_string' ) ) );
}

/**
* Returns registered post meta arguments for a field group.
*
* Only fields that can be represented as single scalar post meta are returned.
* Fields targeting ambiguous locations or storing multiple values are skipped.
*
* @since SCF 6.9.6
*
* @param array $field_group The field group array.
* @return array
*/
function acf_get_field_group_post_meta_for_export( array $field_group ) {
if ( empty( $field_group['show_in_rest'] ) || empty( $field_group['fields'] ) ) {
return array();
}

$post_types = acf_get_field_group_post_types_for_export( $field_group );
if ( empty( $post_types ) ) {
return array();
}

$supported_types = array( 'string', 'boolean', 'integer', 'number' );
$meta = array();

foreach ( $field_group['fields'] as $field ) {
if ( ! is_array( $field ) || empty( $field['name'] ) ) {
continue;
}

if ( is_protected_meta( $field['name'], 'post' ) ) {
continue;
}

if ( isset( $field['allow_in_bindings'] ) && ! $field['allow_in_bindings'] ) {
continue;
}

if ( ! acf_field_type_supports( $field['type'] ?? '', 'bindings', true ) ) {
continue;
}

$field_type = acf_get_field_type( $field['type'] ?? '' );
if ( ! $field_type || empty( $field_type->show_in_rest ) ) {
continue;
}

$schema = acf_get_field_rest_schema( $field );
$types = array_values( array_diff( (array) ( $schema['type'] ?? array() ), array( 'null' ) ) );

// Skip fields that store more than one kind of value, such as checkbox or post object.
if ( 1 !== count( $types ) || ! in_array( $types[0], $supported_types, true ) ) {
continue;
}

$args = array(
'single' => true,
'type' => $types[0],
'show_in_rest' => true,
);

foreach ( $post_types as $post_type ) {
$meta[ $post_type ][ $field['name'] ] = $args;
}
}

return $meta;
}

/**
* Exports registered post meta for eligible field groups as PHP.
*
* @since SCF 6.9.6
*
* @param array $field_groups The field groups being exported.
* @return string
*/
function acf_export_field_groups_post_meta_as_php( array $field_groups ) {
$meta = array();

foreach ( $field_groups as $field_group ) {
foreach ( acf_get_field_group_post_meta_for_export( $field_group ) as $post_type => $fields ) {
foreach ( $fields as $name => $args ) {
$meta[ $post_type ][ $name ] = $args;
}
}
}

if ( empty( $meta ) ) {
return '';
}

$instance = acf_get_internal_post_type_instance( 'acf-field-group' );
$export = "add_action( 'init', function() {\r\n";

foreach ( $meta as $post_type => $fields ) {
foreach ( $fields as $name => $args ) {
$code = var_export( $args, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions -- Used for PHP export.
$code = $instance ? $instance->format_code_for_export( $code ) : $code;

$export .= "\tregister_post_meta( " . var_export( $post_type, true ) . ', ' . var_export( $name, true ) . ", {$code} );\r\n"; // phpcs:ignore WordPress.PHP.DevelopmentFunctions -- Used for PHP export.
}
}

$export .= "} );\r\n\r\n";

return esc_textarea( $export );
}

/**
* acf_get_raw_field_group
*
Expand Down
5 changes: 5 additions & 0 deletions includes/admin/tools/class-acf-admin-tool-export.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ public function html_generate() {
if ( 'acf-post-type' === $post_type ) {
echo acf_export_enter_title_here( $posts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_textarea() used earlier.
}

// Register exported fields as post meta so they work with block bindings.
if ( 'acf-field-group' === $post_type ) {
echo acf_export_field_groups_post_meta_as_php( $posts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_textarea() used earlier.
}
}

echo '</textarea>';
Expand Down
25 changes: 14 additions & 11 deletions tests/e2e/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,22 +144,25 @@ const test = wpTest.extend( {
'role=button[name="View"i]'
);

if (
const hasViewButton =
( await viewButton.count() ) > 0 &&
( await viewButton.first().isVisible() )
) {
return editor.openPreviewPage();
}
( await viewButton.first().isVisible() );

await editorTopBar
.locator( 'role=button[name="Preview"i]' )
.click();
await ( hasViewButton
? viewButton.first()
: editorTopBar.locator( 'role=button[name="Preview"i]' )
).click();

// WordPress trunk renamed the menu item to
// "Preview (opens in a new tab)", so match on the shared
// "Preview ... new tab" text instead of the exact label.
const [ previewPage ] = await Promise.all( [
context.waitForEvent( 'page' ),
page.click(
'role=menuitem[name=/Preview in new tab/i]'
),
page
.getByRole( 'menuitem', {
name: /Preview.*new tab/i,
} )
.click(),
] );

return previewPage;
Expand Down
141 changes: 141 additions & 0 deletions tests/php/includes/test-acf-field-group-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,147 @@ private function make_field_group( $key, $location = array(), $active = true ) {
);
}

// =========================================================================
// Post meta export tests
// =========================================================================

/**
* Builds a REST enabled field group targeting the given post types.
*
* @param array $post_types Post type slugs to target.
* @param array $fields Field definitions.
* @return array
*/
private function make_meta_field_group( $post_types, $fields ) {
$location = array();
foreach ( $post_types as $post_type ) {
$location[] = array(
array(
'param' => 'post_type',
'operator' => '==',
'value' => $post_type,
),
);
}

$field_group = $this->make_field_group( 'group_' . uniqid(), $location );
$field_group['show_in_rest'] = true;
$field_group['fields'] = $fields;

return $field_group;
}

/**
* Test exporting scalar fields as registered post meta.
*/
public function test_export_field_group_post_meta() {
// Field types are loaded by the plugin init hook, which the test bootstrap does not fire.
acf_include( 'includes/fields/class-acf-field-text.php' );

$field_group = $this->make_meta_field_group(
array( 'book' ),
array(
array(
'key' => 'field_title',
'name' => 'book_title',
'type' => 'text',
'allow_in_bindings' => true,
),
)
);

$meta = acf_get_field_group_post_meta_for_export( $field_group );
$code = html_entity_decode( acf_export_field_groups_post_meta_as_php( array( $field_group ) ), ENT_QUOTES, 'UTF-8' );

$this->assertSame( true, $meta['book']['book_title']['single'] );
$this->assertSame( 'string', $meta['book']['book_title']['type'] );
$this->assertStringContainsString( "register_post_meta( 'book', 'book_title'", $code );
$this->assertStringContainsString( "'show_in_rest' => true", $code );
}

/**
* Test registering a field for every post type the group targets.
*/
public function test_export_field_group_post_meta_multiple_post_types() {
acf_include( 'includes/fields/class-acf-field-text.php' );

$field_group = $this->make_meta_field_group(
array( 'book', 'movie' ),
array(
array(
'name' => 'subtitle',
'type' => 'text',
),
)
);

$meta = acf_get_field_group_post_meta_for_export( $field_group );

$this->assertArrayHasKey( 'book', $meta );
$this->assertArrayHasKey( 'movie', $meta );
}

/**
* Test skipping fields that should not be exported as post meta.
*/
public function test_export_field_group_post_meta_skips_ineligible_fields() {
acf_include( 'includes/fields/class-acf-field-text.php' );
acf_include( 'includes/fields/class-acf-field-select.php' );
acf_include( 'includes/fields/class-acf-field-checkbox.php' );

$field_group = $this->make_meta_field_group(
array( 'book' ),
array(
array(
'name' => 'hidden_title',
'type' => 'text',
'allow_in_bindings' => false,
),
array(
'name' => 'multiple_values',
'type' => 'checkbox',
'allow_in_bindings' => true,
'choices' => array(
'one' => 'One',
'two' => 'Two',
),
),
array(
'name' => '_protected_title',
'type' => 'text',
),
)
);

$this->assertSame( array(), acf_get_field_group_post_meta_for_export( $field_group ) );
}

/**
* Test skipping field groups without a concrete post type location.
*/
public function test_export_field_group_post_meta_skips_ambiguous_location() {
acf_include( 'includes/fields/class-acf-field-text.php' );

$field_group = $this->make_meta_field_group(
array( 'book' ),
array(
array(
'name' => 'book_title',
'type' => 'text',
),
)
);

// Add a second rule to the same location group so it is no longer a single post type.
$field_group['location'][0][] = array(
'param' => 'post_status',
'operator' => '==',
'value' => 'publish',
);

$this->assertSame( array(), acf_get_field_group_post_meta_for_export( $field_group ) );
}

// =========================================================================
// SECTION 1: Field Group Key Validation
// =========================================================================
Expand Down
Loading