• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to build custom ACF Pro dynamic fields extensions utilizing modern Filesystem API schemas

How to build custom ACF Pro dynamic fields extensions utilizing modern Filesystem API schemas

Leveraging the Filesystem API for Advanced ACF Pro Dynamic Field Extensions

Advanced Custom Fields (ACF) Pro offers a powerful mechanism for creating dynamic field sources, allowing developers to populate select, radio, checkbox, and other choice-based fields with data from external or custom sources. While ACF provides built-in options like post object selection or taxonomy terms, complex scenarios often demand custom data retrieval. This guide delves into extending ACF Pro’s dynamic field capabilities by architecting solutions that interact with modern filesystem schemas, specifically focusing on JSON and YAML data sources. We’ll explore how to build robust, reusable extensions that enhance data management within WordPress.

Understanding ACF Dynamic Field Sources

ACF Pro’s dynamic field functionality is primarily driven by the `acf/load_field/type={$field_type}` filter. This filter allows you to hook into the loading process of specific field types and modify their choices programmatically. The core of this process involves returning an array of choices, where each choice is an associative array with `value` and `label` keys.

When configuring a field in ACF to use a dynamic source, you specify a `data_source` array within the field’s settings. This array typically includes:

  • type: The type of dynamic source (e.g., ‘post’, ‘taxonomy’, ‘user’).
  • value: The key to use for the choice’s value (e.g., ‘ID’).
  • label: The key to use for the choice’s label (e.g., ‘post_title’).
  • return_format: How the value should be returned (e.g., ‘value’, ‘array’).

For custom sources, we’ll be defining our own `type` and providing a callback function to handle the data retrieval and formatting.

Designing a Filesystem-Based Data Provider

Our goal is to create a dynamic field source that reads data from structured files on the server’s filesystem. JSON and YAML are excellent candidates due to their human-readability and widespread adoption. We’ll abstract the file reading and parsing logic into a reusable class.

Consider a scenario where we have a directory containing configuration files for various services, and we want to offer a dropdown of available service names. Each service might have its own JSON file.

Implementing the Data Provider Class

We’ll create a PHP class that handles reading and parsing these files. For robust file handling, we’ll leverage WordPress’s built-in filesystem functions where appropriate, or standard PHP file operations.

Let’s define a base class for our filesystem data providers. This class will manage the base directory and provide methods for reading and parsing files.

Base Filesystem Data Provider

This abstract class will enforce the structure for concrete providers.

<?php
/**
 * Abstract base class for filesystem-based ACF dynamic field data providers.
 */
abstract class ACF_Filesystem_Dynamic_Provider {

    /**
     * The base directory where data files are located.
     *
     * @var string
     */
    protected $base_dir;

    /**
     * Constructor.
     *
     * @param string $base_dir The base directory for data files.
     */
    public function __construct( string $base_dir ) {
        // Ensure the base directory is absolute and ends with a slash.
        $this->base_dir = trailingslashit( realpath( $base_dir ) );
    }

    /**
     * Retrieves the data source type identifier.
     *
     * @return string
     */
    abstract public function get_type(): string;

    /**
     * Retrieves the data for the dynamic field.
     *
     * @param array $field The ACF field array.
     * @return array An array of choices for the ACF field.
     */
    abstract public function get_choices( array $field ): array;

    /**
     * Reads and parses a file from the filesystem.
     *
     * @param string $filename The name of the file to read.
     * @return mixed|false Parsed file content or false on failure.
     */
    protected function read_file( string $filename ) {
        $filepath = $this->base_dir . $filename;

        if ( ! file_exists( $filepath ) || ! is_readable( $filepath ) ) {
            error_log( "ACF Filesystem Provider: File not found or not readable: {$filepath}" );
            return false;
        }

        $content = file_get_contents( $filepath );
        if ( $content === false ) {
            error_log( "ACF Filesystem Provider: Failed to read file: {$filepath}" );
            return false;
        }

        return $this->parse_content( $content, $filepath );
    }

    /**
     * Parses the file content. This method must be implemented by subclasses.
     *
     * @param string $content The raw file content.
     * @param string $filepath The full path to the file.
     * @return mixed Parsed content.
     */
    abstract protected function parse_content( string $content, string $filepath );

    /**
     * Formats the raw data into ACF choice array format.
     *
     * @param array $data The raw data retrieved from the file.
     * @param array $field The ACF field array.
     * @return array Formatted choices.
     */
    protected function format_choices( array $data, array $field ): array {
        $choices = [];
        $value_key = $field['value'] ?? 'value';
        $label_key = $field['label'] ?? 'label';

        foreach ( $data as $item ) {
            if ( ! is_array( $item ) ) {
                continue; // Skip if item is not an associative array
            }
            if ( ! isset( $item[ $value_key ] ) || ! isset( $item[ $label_key ] ) ) {
                continue; // Skip if required keys are missing
            }
            $choices[ $item[ $value_key ] ] = $item[ $label_key ];
        }
        return $choices;
    }
}

JSON Data Provider

This concrete provider will handle JSON files.

<?php
/**
 * ACF Dynamic Field Provider for JSON files.
 */
class ACF_JSON_Dynamic_Provider extends ACF_Filesystem_Dynamic_Provider {

    /**
     * @inheritDoc
     */
    public function get_type(): string {
        return 'json_file'; // Unique identifier for this provider type
    }

    /**
     * @inheritDoc
     */
    protected function parse_content( string $content, string $filepath ) {
        $data = json_decode( $content, true );
        if ( json_last_error() !== JSON_ERROR_NONE ) {
            error_log( "ACF Filesystem Provider (JSON): JSON decode error in {$filepath}: " . json_last_error_msg() );
            return false;
        }
        // Expecting an array of objects/associative arrays
        if ( ! is_array( $data ) ) {
            error_log( "ACF Filesystem Provider (JSON): Expected an array in {$filepath}, got " . gettype( $data ) );
            return false;
        }
        return $data;
    }

    /**
     * @inheritDoc
     */
    public function get_choices( array $field ): array {
        // The filename is expected to be passed in the field's 'data_source' array.
        $filename = $field['data_source']['filename'] ?? '';

        if ( empty( $filename ) ) {
            error_log( "ACF Filesystem Provider (JSON): 'filename' not specified in data_source for field: {$field['name']}" );
            return [];
        }

        $data = $this->read_file( $filename );

        if ( $data === false || ! is_array( $data ) ) {
            return [];
        }

        return $this->format_choices( $data, $field );
    }
}

YAML Data Provider

For YAML, we’ll need a YAML parsing library. The most common is `symfony/yaml`. If you’re using Composer for your plugin, you can include it.

First, ensure you have the Symfony YAML component installed:

composer require symfony/yaml

Then, implement the YAML provider:

<?php
// Ensure Composer's autoloader is included if this is a standalone plugin
// require_once __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;

/**
 * ACF Dynamic Field Provider for YAML files.
 */
class ACF_YAML_Dynamic_Provider extends ACF_Filesystem_Dynamic_Provider {

    /**
     * @inheritDoc
     */
    public function get_type(): string {
        return 'yaml_file'; // Unique identifier for this provider type
    }

    /**
     * @inheritDoc
     */
    protected function parse_content( string $content, string $filepath ) {
        try {
            $data = Yaml::parse( $content );
            // Expecting an array of objects/associative arrays
            if ( ! is_array( $data ) ) {
                error_log( "ACF Filesystem Provider (YAML): Expected an array in {$filepath}, got " . gettype( $data ) );
                return false;
            }
            return $data;
        } catch ( ParseException $e ) {
            error_log( "ACF Filesystem Provider (YAML): YAML parse error in {$filepath}: " . $e->getMessage() );
            return false;
        }
    }

    /**
     * @inheritDoc
     */
    public function get_choices( array $field ): array {
        // The filename is expected to be passed in the field's 'data_source' array.
        $filename = $field['data_source']['filename'] ?? '';

        if ( empty( $filename ) ) {
            error_log( "ACF Filesystem Provider (YAML): 'filename' not specified in data_source for field: {$field['name']}" );
            return [];
        }

        $data = $this->read_file( $filename );

        if ( $data === false || ! is_array( $data ) ) {
            return [];
        }

        return $this->format_choices( $data, $field );
    }
}

Registering Custom Dynamic Field Sources

To make these providers available within ACF’s dynamic field settings, we need to register them using the `acf/get_field_type_args` filter. This filter allows us to add custom types and associate them with our provider classes.

Registration Logic

We’ll need a central place to manage our provider instances and their registration. A simple class or a set of functions can handle this.

<?php
/**
 * Manages ACF custom dynamic field providers.
 */
class ACF_Custom_Dynamic_Providers {

    /**
     * Stores instances of registered providers.
     *
     * @var array
     */
    private static $providers = [];

    /**
     * The base directory for all data files.
     * This should be configured based on your plugin's structure.
     * For example, a 'data' directory within your plugin's root.
     *
     * @var string
     */
    private static $global_base_dir;

    /**
     * Initializes the custom providers.
     * Call this function early in your plugin's lifecycle (e.g., in an 'plugins_loaded' action).
     */
    public static function init() {
        // Define your global base directory.
        // Example: Assuming a 'data' folder in your plugin's root.
        // Replace 'your-plugin-directory' with the actual slug of your plugin.
        self::$global_base_dir = plugin_dir_path( __FILE__ ) . '../data/'; // Adjust path as needed

        // Ensure the base directory exists and is writable if necessary for certain operations.
        if ( ! file_exists( self::$global_base_dir ) ) {
            // Optionally create it, but be mindful of permissions.
            // wp_mkdir_p( self::$global_base_dir );
        }

        // Register the JSON provider.
        self::register_provider( new ACF_JSON_Dynamic_Provider( self::$global_base_dir ) );

        // Register the YAML provider.
        self::register_provider( new ACF_YAML_Dynamic_Provider( self::$global_base_dir ) );

        // Hook into ACF to provide the callback functions.
        add_filter( 'acf/get_field_type_args', [ __CLASS__, 'register_acf_types' ] );
    }

    /**
     * Registers a provider instance.
     *
     * @param ACF_Filesystem_Dynamic_Provider $provider The provider instance.
     */
    private static function register_provider( ACF_Filesystem_Dynamic_Provider $provider ) {
        self::$providers[ $provider->get_type() ] = $provider;
    }

    /**
     * Filters ACF field type arguments to add our custom dynamic sources.
     *
     * @param array $args Field type arguments.
     * @return array Modified field type arguments.
     */
    public static function register_acf_types( array $args ): array {
        // Add our custom types to the 'data_sources' array for relevant field types.
        $custom_data_sources = [];
        foreach ( self::$providers as $type => $provider ) {
            $custom_data_sources[ $type ] = [
                'label' => ucwords( str_replace( '_', ' ', $type ) ), // e.g., "Json File", "Yaml File"
                'type'  => $type,
                // We don't need to specify 'value', 'label', 'return_format' here,
                // as they will be passed directly to the get_choices method via the field's data_source.
            ];
        }

        // Apply to field types that support dynamic sources.
        $supported_field_types = [ 'select', 'radio', 'checkbox', 'button_group', 'select_advanced' ];

        foreach ( $supported_field_types as $field_type ) {
            if ( isset( $args[ $field_type ]['data_sources'] ) ) {
                // Merge our custom sources with existing ones.
                $args[ $field_type ]['data_sources'] = array_merge( $args[ $field_type ]['data_sources'], $custom_data_sources );
            }
        }

        return $args;
    }

    /**
     * Retrieves the appropriate provider instance for a given field type.
     *
     * @param string $type The provider type.
     * @return ACF_Filesystem_Dynamic_Provider|null The provider instance or null if not found.
     */
    public static function get_provider( string $type ): ?ACF_Filesystem_Dynamic_Provider {
        return self::$providers[ $type ] ?? null;
    }
}

// Initialize the custom providers.
// This should be called within your plugin's main file or an initialization hook.
// Example:
// add_action( 'plugins_loaded', [ 'ACF_Custom_Dynamic_Providers', 'init' ] );

Hooking into ACF’s Field Loading

Once registered, ACF will recognize our custom types. However, we still need to tell ACF *which* provider class to use when a field is configured with our custom `type`. This is done via the `acf/load_field/type={$field_type}` filter.

<?php
/**
 * Callback function for ACF dynamic fields.
 *
 * @param array $field The ACF field array.
 * @return array The modified ACF field array with choices populated.
 */
function my_acf_load_dynamic_field_choices( array $field ): array {
    // Check if this field is configured for a custom dynamic source.
    if ( ! isset( $field['data_source']['type'] ) || empty( $field['data_source']['type'] ) ) {
        return $field; // Not a dynamic field or no type specified.
    }

    $provider_type = $field['data_source']['type'];
    $provider = ACF_Custom_Dynamic_Providers::get_provider( $provider_type );

    if ( ! $provider ) {
        error_log( "ACF Filesystem Provider: No provider found for type: {$provider_type}" );
        return $field; // Provider not found.
    }

    // Get the choices from our provider.
    $choices = $provider->get_choices( $field );

    if ( is_array( $choices ) ) {
        $field['choices'] = $choices;
    } else {
        // Handle cases where the provider might return an error or empty result.
        $field['choices'] = [];
        error_log( "ACF Filesystem Provider: Failed to retrieve choices for field {$field['name']} (type: {$provider_type})" );
    }

    // Ensure 'value' and 'label' keys are correctly mapped if not default.
    // ACF's format_choices in the provider handles this, but it's good to be aware.
    // The 'return_format' from the field's data_source is handled by ACF itself
    // after our choices are populated.

    return $field;
}

// Hook into the filter for all relevant field types.
// Use a lower priority to ensure it runs after ACF's default loaders.
$supported_field_types = [ 'select', 'radio', 'checkbox', 'button_group', 'select_advanced' ];
foreach ( $supported_field_types as $field_type ) {
    add_filter( "acf/load_field/type={$field_type}", 'my_acf_load_dynamic_field_choices', 20, 1 );
}

Setting Up Data Files

Create a directory structure for your data files. For instance, if your `global_base_dir` is set to `wp-content/plugins/your-plugin/data/`, you would place your files there.

Example JSON Data File (`services.json`)

[
    {
        "id": "svc_001",
        "name": "Authentication Service",
        "description": "Handles user login and registration."
    },
    {
        "id": "svc_002",
        "name": "Payment Gateway",
        "description": "Processes financial transactions."
    },
    {
        "id": "svc_003",
        "name": "Notification System",
        "description": "Sends emails, SMS, and push notifications."
    }
]

Example YAML Data File (`regions.yaml`)

- code: "us-east-1"
  name: "US East (N. Virginia)"
  continent: "North America"

- code: "eu-west-2"
  name: "Europe (London)"
  continent: "Europe"

- code: "ap-southeast-1"
  name: "Asia Pacific (Singapore)"
  continent: "Asia"

Configuring ACF Fields

Now, in the ACF Field Group editor, create a new field (e.g., a ‘Select’ field). In the Field Type settings, choose ‘Select’ (or your desired field type). Scroll down to the ‘Data Source’ section.

Configuring for JSON

Set the following:

  • Data Source Type: Select ‘JSON File’ (or whatever label you defined in `register_acf_types`).
  • Value: Enter id (the key in your JSON objects for the value).
  • Label: Enter name (the key in your JSON objects for the display label).
  • Return Format: Choose ‘Value’.

Crucially, you need to tell the provider *which* file to read. This is done by passing an additional parameter to the `data_source` array. ACF allows custom parameters here. We’ll use the `filename` parameter.

Under the ‘Data Source’ section, you’ll see an option for ‘Additional Data Source Parameters’ or similar. You might need to enable ‘Advanced Settings’ or look for a custom key-value input. Enter:

filename: services.json

ACF will pass this `filename` parameter to our `get_choices` method within the `ACF_JSON_Dynamic_Provider`.

Configuring for YAML

Similarly, for the YAML example:

  • Data Source Type: Select ‘YAML File’.
  • Value: Enter code.
  • Label: Enter name.
  • Return Format: Choose ‘Value’.

And in the ‘Additional Data Source Parameters’:

filename: regions.yaml

Advanced Considerations and Best Practices

Error Handling and Logging

Robust error logging is critical. The provided examples include `error_log` calls for file access issues, parsing errors, and missing configuration. Ensure your WordPress debug log is enabled (`WP_DEBUG` and `WP_DEBUG_LOG` in `wp-config.php`) to monitor these messages.

Caching

Reading files from the filesystem on every page load can be inefficient, especially for large files or complex parsing. Implement caching for the data retrieved from files. WordPress Transients API is an excellent choice for this.

// Example of adding caching to ACF_JSON_Dynamic_Provider::get_choices
public function get_choices( array $field ): array {
    $filename = $field['data_source']['filename'] ?? '';
    if ( empty( $filename ) ) {
        // ... error handling ...
        return [];
    }

    // Generate a unique cache key.
    $cache_key = 'acf_dynamic_choices_' . md5( $this->base_dir . $filename );
    $cached_data = get_transient( $cache_key );

    if ( $cached_data !== false ) {
        return $cached_data; // Return cached choices.
    }

    $data = $this->read_file( $filename );
    if ( $data === false || ! is_array( $data ) ) {
        return [];
    }

    $choices = $this->format_choices( $data, $field );

    // Cache the choices for a reasonable duration (e.g., 1 hour).
    set_transient( $cache_key, $choices, HOUR_IN_SECONDS );

    return $choices;
}

Security

Ensure that the `base_dir` is properly secured and that the files being read do not contain sensitive information that should not be exposed. Avoid reading files from user-uploaded directories unless strictly necessary and with proper sanitization and validation.

File Structure and Organization

Organize your data files logically. For larger projects, consider subdirectories within your `base_dir` (e.g., `data/services/`, `data/regions/`). Your provider logic might need to be extended to handle these subdirectories, perhaps by allowing a `path` parameter in the `data_source` configuration.

Dynamic File Discovery

Instead of hardcoding filenames, you could extend the providers to discover all `.json` or `.yaml` files within a specified directory. This would make the system more dynamic, allowing new files to be added without code changes.

// Example: Modifying ACF_JSON_Dynamic_Provider to discover files
public function get_choices( array $field ): array {
    // Allow specifying a subdirectory or use the base dir.
    $subdir = $field['data_source']['path'] ?? '';
    $directory_to_scan = trailingslashit( $this->base_dir . $subdir );

    if ( ! is_dir( $directory_to_scan ) ) {
        error_log( "ACF Filesystem Provider (JSON): Directory not found: {$directory_to_scan}" );
        return [];
    }

    $files = glob( $directory_to_scan . '*.json' ); // Find all JSON files
    $all_choices = [];

    foreach ( $files as $filepath ) {
        $filename = basename( $filepath );
        $data = $this->read_file( $filename ); // read_file needs to handle relative paths from base_dir

        if ( $data === false || ! is_array( $data ) ) {
            continue;
        }

        // You might want to prefix choices or group them by filename.
        // For simplicity, let's just merge them, assuming unique values across files.
        // A better approach might be to return an array of arrays, grouped by filename.
        $formatted = $this->format_choices( $data, $field );
        $all_choices = array_merge( $all_choices, $formatted );
    }

    return $all_choices;
}

When using file discovery, the `data_source` configuration for the ACF field might not need a `filename` parameter, but rather a `path` parameter to specify the subdirectory to scan.

Conclusion

By extending ACF Pro with custom dynamic field sources that interact with filesystem data, you can build highly flexible and maintainable content management systems. The ability to read from structured files like JSON and YAML provides a powerful way to decouple data from the WordPress database, enabling easier configuration management, external data integration, and more sophisticated content modeling. Remember to prioritize error handling, caching, and security for production-ready solutions.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (662)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (5)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Server (118)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (726)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala