How to build custom ACF Pro dynamic fields extensions utilizing modern Rewrite API custom endpoints schemas
Leveraging the Rewrite API for Advanced ACF Pro Dynamic Field Extensions
Customizing Advanced Custom Fields (ACF) Pro beyond its built-in dynamic field capabilities often requires deeper integration with WordPress’s core functionalities. While ACF offers hooks for modifying field values and choices, creating entirely new dynamic field *types* or complex data-driven field behaviors necessitates a more robust approach. This post details how to architect and implement custom ACF Pro dynamic field extensions by leveraging the WordPress Rewrite API to create custom REST API endpoints, enabling sophisticated data retrieval and manipulation for your fields.
Understanding the Need for Custom Endpoints
ACF Pro’s dynamic field features, such as “Dynamically Populate Choices,” are powerful for populating select fields, radio buttons, and checkboxes from custom queries or external data sources. However, these features are primarily designed for static data retrieval during the rendering of the post edit screen. When you need:
- Real-time data fetching that updates field options without a page reload (e.g., live search suggestions).
- Complex data filtering and sorting logic that is too intricate for simple ACF query parameters.
- Integration with external APIs where authentication or complex request/response handling is required.
- Dynamic field behavior that depends on user roles or other contextual information not directly available to ACF’s choice population hooks.
- Creating entirely new field types that require programmatic data generation or manipulation.
…you’ll find the standard ACF dynamic population methods insufficient. This is where the WordPress Rewrite API and its integration with the REST API become invaluable. By creating custom REST API endpoints, we can expose a structured data interface that ACF fields can query asynchronously.
Architecting the Solution: Plugin Structure and REST API Registration
We’ll build a custom WordPress plugin to house our ACF extension. This plugin will register a custom REST API route using register_rest_route(). This function is the cornerstone of extending the WordPress REST API and allows us to define our own endpoints, specify their methods (GET, POST, etc.), and hook in callback functions to handle requests.
Consider a scenario where we need an ACF field to dynamically populate with a list of products from a custom post type, with advanced filtering capabilities (e.g., by category, stock status, and price range). We’ll create an endpoint like /my-plugin/v1/products.
Plugin Initialization and REST Route Registration
The core of our plugin will involve hooking into the rest_api_init action. This action fires when the REST API is being initialized, providing the perfect opportunity to register our custom routes.
my-acf-dynamic-fields.php (Main Plugin File)
<?php
/**
* Plugin Name: My ACF Dynamic Fields
* Description: Extends ACF Pro with custom dynamic fields using REST API endpoints.
* Version: 1.0.0
* Author: Your Name
* Text Domain: my-acf-dynamic-fields
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Register custom REST API routes.
*/
function my_acf_dynamic_fields_register_routes() {
// Register the products endpoint.
register_rest_route( 'my-acf-dynamic-fields/v1', '/products', array(
'methods' => WP_REST_Server::READABLE, // Equivalent to 'GET'
'callback' => 'my_acf_dynamic_fields_get_products',
'permission_callback' => '__return_true', // For simplicity, allow public access. Adjust for security.
'args' => array(
'search' => array(
'required' => false,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'description' => 'Search term for product name.',
),
'category' => array(
'required' => false,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'description' => 'Product category slug.',
),
'in_stock' => array(
'required' => false,
'type' => 'boolean',
'description' => 'Filter for products in stock.',
),
'min_price' => array(
'required' => false,
'type' => 'number',
'description' => 'Minimum price filter.',
),
'max_price' => array(
'required' => false,
'type' => 'number',
'description' => 'Maximum price filter.',
),
'limit' => array(
'required' => false,
'type' => 'integer',
'default' => 20,
'minimum' => 1,
'maximum' => 100,
'description' => 'Number of results to return.',
),
'page' => array(
'required' => false,
'type' => 'integer',
'default' => 1,
'minimum' => 1,
'description' => 'Page number for pagination.',
),
),
) );
}
add_action( 'rest_api_init', 'my_acf_dynamic_fields_register_routes' );
/**
* Callback function to retrieve products.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
function my_acf_dynamic_fields_get_products( WP_REST_Request $request ) {
$args = array(
'post_type' => 'product', // Assuming 'product' is your custom post type slug.
'post_status' => 'publish',
'posts_per_page' => $request->get_param( 'limit' ),
'paged' => $request->get_param( 'page' ),
'meta_query' => array(
'relation' => 'AND',
),
'tax_query' => array(
'relation' => 'AND',
),
);
// Apply search term.
$search_term = $request->get_param( 'search' );
if ( ! empty( $search_term ) ) {
// This is a basic search. For more advanced search, consider WP_Query's 's' parameter or a dedicated search plugin.
$args['s'] = $search_term;
}
// Apply category filter.
$category_slug = $request->get_param( 'category' );
if ( ! empty( $category_slug ) ) {
$args['tax_query'][] = array(
'taxonomy' => 'product_category', // Assuming 'product_category' is your taxonomy slug.
'field' => 'slug',
'terms' => $category_slug,
);
}
// Apply in_stock filter.
$in_stock = $request->get_param( 'in_stock' );
if ( $in_stock !== null ) { // Check for null as false is a valid boolean value.
$stock_status = $in_stock ? 'instock' : 'outofstock';
$args['meta_query'][] = array(
'key' => '_stock_status', // Example meta key for stock status.
'value' => $stock_status,
'compare' => '=',
);
}
// Apply price filters.
$min_price = $request->get_param( 'min_price' );
$max_price = $request->get_param( 'max_price' );
if ( $min_price !== null ) {
$args['meta_query'][] = array(
'key' => '_price', // Example meta key for price.
'value' => $min_price,
'type' => 'NUMERIC',
'compare' => '>=',
);
}
if ( $max_price !== null ) {
$args['meta_query'][] = array(
'key' => '_price', // Example meta key for price.
'value' => $max_price,
'type' => 'NUMERIC',
'compare' => '<=',
);
}
// Remove empty meta_query or tax_query if no filters were applied.
if ( count( $args['meta_query'] ) === 1 ) { // Only the 'relation' key exists.
unset( $args['meta_query'] );
}
if ( count( $args['tax_query'] ) === 1 ) { // Only the 'relation' key exists.
unset( $args['tax_query'] );
}
$query = new WP_Query( $args );
$products_data = array();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$product_id = get_the_ID();
$products_data[] = array(
'id' => $product_id,
'text' => get_the_title(), // Or a more descriptive title/name.
'value' => $product_id, // The value to be stored in ACF.
// Add more relevant data if needed by the frontend.
'price' => get_post_meta( $product_id, '_price', true ),
'stock' => get_post_meta( $product_id, '_stock_status', true ),
);
}
wp_reset_postdata();
}
// Prepare response.
$response_data = array(
'products' => $products_data,
'total_products' => $query->found_posts,
'max_num_pages' => $query->max_num_pages,
);
$response = new WP_REST_Response( $response_data, 200 );
$response->add_links( array(
'self' => rest_url( 'my-acf-dynamic-fields/v1/products' ),
// Add pagination links if needed.
) );
return $response;
}
// Include other endpoint callbacks here if necessary.
// For example, a callback to get product categories.
function my_acf_dynamic_fields_get_categories( WP_REST_Request $request ) {
$categories = get_terms( array(
'taxonomy' => 'product_category', // Assuming 'product_category' is your taxonomy slug.
'hide_empty' => true,
) );
$categories_data = array();
if ( ! is_wp_error( $categories ) ) {
foreach ( $categories as $category ) {
$categories_data[] = array(
'text' => $category->name,
'value' => $category->slug,
);
}
}
return new WP_REST_Response( $categories_data, 200 );
}
function my_acf_dynamic_fields_register_category_route() {
register_rest_route( 'my-acf-dynamic-fields/v1', '/categories', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'my_acf_dynamic_fields_get_categories',
'permission_callback' => '__return_true',
) );
}
add_action( 'rest_api_init', 'my_acf_dynamic_fields_register_category_route' );
In this code:
- We define a custom route
/my-acf-dynamic-fields/v1/products. WP_REST_Server::READABLEspecifies that this endpoint responds to GET requests.my_acf_dynamic_fields_get_productsis the callback function that will handle incoming requests to this endpoint.- The
argsarray defines the expected query parameters, their types, whether they are required, and importantly, theirsanitize_callback. This is crucial for security. - The
permission_callbackis set to__return_truefor simplicity. In a production environment, you would implement robust checks here (e.g., checking user capabilities, nonces, or API keys). - The
my_acf_dynamic_fields_get_productsfunction constructs aWP_Querybased on the sanitized parameters. - It iterates through the results, formatting them into an array suitable for an ACF field (typically with
'text'for display and'value'for storage). - The response is wrapped in a
WP_REST_Responseobject, which includes the data and an HTTP status code. - We also added a
/categoriesendpoint as an example of how to fetch taxonomy terms.
Integrating with ACF Pro Fields
Now that we have our REST API endpoints set up, we need to configure ACF Pro fields to utilize them. This is typically done within the ACF Field Group editor. For fields like “Select,” “Checkbox,” “Radio Button,” or “Button Group,” you can enable “Dynamically Populate Choices.”
Configuring a “Select” Field for Dynamic Product Population
When editing a field group in ACF Pro, locate the field you want to make dynamic (e.g., a “Select” field named “Related Product”). Enable the “Dynamically Populate Choices” option. This will reveal several new settings:
ACF Field Settings (UI Description)
- Filter by Post Type: Set this to “None” as we are using a custom endpoint.
- Post Type: Leave blank.
- Taxonomy: Leave blank.
- Post Status: Leave blank.
- Post Order: Leave blank.
- Dynamically Populate Choices: Yes.
- Query Type: Select “Custom Endpoint”.
- Endpoint URL: Enter the full URL to your custom endpoint. For example:
[site_url]/wp-json/my-acf-dynamic-fields/v1/products. You can use the[site_url]placeholder, which ACF will resolve. - Field Type: Select “Select”.
- Value Format: This determines what part of the JSON response is used for the field’s value. Typically, this will be
value(matching our API output). - Label Format: This determines what part of the JSON response is used for the field’s display text. Typically, this will be
text(matching our API output). - Return Format: Choose how the value should be saved (e.g., “Value”).
- Allow Null: Yes/No.
- Placeholder: Optional placeholder text.
ACF Pro will automatically append the query parameters defined in your REST API route to the Endpoint URL when it makes the request. For instance, if you add a “Search” field to your ACF field group (e.g., a text input field), ACF will automatically send its value as the search parameter to your /products endpoint.
Handling Dynamic Search and Filtering in ACF
ACF Pro’s dynamic population feature includes built-in support for search inputs. When you add a text input field to your ACF field group and configure your dynamic field to use it for searching, ACF will automatically pass the input’s value as a query parameter to your endpoint. You need to ensure your endpoint’s args definition matches the parameter name ACF sends (e.g., search).
For more complex filtering (like dropdowns for categories or checkboxes for stock status), you’ll need to:
- Add additional ACF fields (e.g., a “Select” field for categories, populated by our
/categoriesendpoint). - Use ACF’s
acf/load_fieldfilter to dynamically modify theendpoint_argsfor your dynamic field based on the values of these other filter fields.
Example: Modifying Endpoint Arguments with acf/load_field
<?php
/**
* Modify endpoint arguments for the 'Related Product' field based on filter fields.
*
* @param array $field The field array.
* @return array The modified field array.
*/
function my_acf_dynamic_fields_modify_product_endpoint_args( $field ) {
// Ensure this is the correct field we want to modify.
if ( 'select' === $field['type'] && 'Related Product' === $field['label'] ) {
// Get the value of the 'product_category' filter field.
$category_slug = get_field( 'product_category_filter' ); // Assuming you have a field named 'product_category_filter'.
// Get the value of the 'product_stock_status' filter field.
$stock_status_value = get_field( 'product_stock_status_filter' ); // Assuming a field that returns 'instock' or 'outofstock'.
// Initialize endpoint_args if it doesn't exist.
if ( ! isset( $field['endpoint_args'] ) || ! is_array( $field['endpoint_args'] ) ) {
$field['endpoint_args'] = array();
}
// Add category argument if a category is selected.
if ( ! empty( $category_slug ) ) {
$field['endpoint_args']['category'] = $category_slug;
}
// Add stock status argument.
if ( ! empty( $stock_status_value ) ) {
// ACF sends boolean for checkboxes, but our API expects string 'instock'/'outofstock'.
// We need to map this. If the filter field is a select/radio, it might directly return the string.
// Adjust this logic based on your filter field's return format.
$field['endpoint_args']['in_stock'] = ( 'instock' === $stock_status_value );
}
// You can also dynamically set the endpoint URL if needed.
// $field['endpoint_url'] = '[site_url]/wp-json/my-acf-dynamic-fields/v1/products';
}
return $field;
}
add_filter( 'acf/load_field/name=related_product', 'my_acf_dynamic_fields_modify_product_endpoint_args' ); // Replace 'related_product' with your field's actual name.
// Or use 'acf/load_field' to target by type or other properties if needed.
// add_filter( 'acf/load_field', 'my_acf_dynamic_fields_modify_product_endpoint_args' );
In this example:
- We hook into
acf/load_field, targeting a specific field by its name ('related_product'). - Inside the callback, we retrieve the values from other ACF fields (
'product_category_filter'and'product_stock_status_filter') usingget_field(). - We then append these values as query parameters (
categoryandin_stock) to the$field['endpoint_args']array. ACF Pro automatically merges these arguments with the baseendpoint_url. - Note the conversion of the stock status value. Ensure your filter field’s return format aligns with what your API expects.
Security Considerations
Exposing API endpoints, even for internal use, requires careful security considerations:
- Authentication and Authorization: The
permission_callbackinregister_rest_routeis critical. For internal use, you might check user capabilities (e.g.,current_user_can('edit_posts')). For external access, implement API keys, OAuth, or JWT. - Input Validation and Sanitization: Always sanitize all incoming data using WordPress functions like
sanitize_text_field(),absint(),esc_url_raw(), etc. Theargsarray inregister_rest_routeis the primary place for this. - Rate Limiting: Protect your endpoints from abuse by implementing rate limiting, especially if they are publicly accessible.
- Data Exposure: Ensure your API only returns necessary data. Avoid exposing sensitive information.
- Nonces: For actions that modify data (POST, PUT, DELETE), always use nonces to verify requests.
Advanced Use Cases and Further Development
This pattern opens doors to numerous advanced ACF extensions:
- Real-time Autocomplete Fields: Use the
searchparameter for live suggestions as a user types. - Dependent Dynamic Fields: Chain multiple dynamic fields where the endpoint for one field depends on the selection of another. This can be achieved by using
acf/load_fieldto dynamically set theendpoint_urlorendpoint_args. - Custom Field Types: While this post focuses on populating choices, you could extend this to create entirely new field types by registering them with ACF and having their rendering logic fetch data from your custom endpoints.
- Integration with External Services: Build endpoints that fetch data from third-party APIs, perform transformations, and return it in a format ACF can use.
- Complex Data Visualization: Use custom endpoints to feed data into JavaScript-based charting libraries integrated within ACF’s flexible content fields or custom blocks.
By combining the power of the WordPress REST API and the flexibility of ACF Pro’s dynamic population, developers can build highly sophisticated and data-driven user interfaces within the WordPress admin, significantly enhancing content management workflows.