
Advanced Custom Fields (ACF) is an amazing plugin that can revolutionize the way you build and edit posts inside WordPress. Two of the more common ACF features are repeater fields and flexible content fields, which can make the post editing experience much more intuitive. In fact, I can’t recall the last website I build where I did not use ACF repeater or flexible content fields.
ACF is great, but…
There are a couple of things about ACF that leave something to be desired, one of which is the function get_field()
. This function is extremely convenient when working with repeater and flexible content fields because it retrieves all of the pertinent meta values and puts them into a nice multidimensional array; however, it is also expensive when it comes to performance.
In a classic post by Bill Erickson, he recommended not using ACF functions like get_field()
largely because of poor performance. Bill recommends using the native WordPress function get_post_meta()
to retrieve post meta values because it’s faster and won’t result in the dreaded white screen of death that could happen if/when ACF is deactivated. Craig Simpson also did a nice writeup about performance considerations when working with ACF, along with the numbers to prove it.
WET Code Stinks
Now there’s a slight problem with using get_post_meta()
vs. get_field()
: it can be a bit of a pain to write the functions that retrieve the post meta values for all those repeater subfields and flexible content layouts.
What’s worse is that because these ACF field groups are often tailored for each client, you usually end up rewriting these functions on every project. And WET (Written Every Time) code is something that no developer is proud of.
Is There a Better Way?
In a recent project I challenged myself to find a better way to get the ACF post meta values. I was hoping to come up with a function that could be called once and return a multidimensional array with all the meta values that I needed. In essence, I was looking for a direct replacement for get_field()
.
This is the recursive function that I came up with:
<?php | |
/** | |
* Retrieves all post meta data according to the structure in the $config | |
* array. | |
* | |
* Provides a convenient and more performant alternative to ACF's | |
* `get_field()`. | |
* | |
* This function is especially useful when working with ACF repeater fields and | |
* flexible content layouts. | |
* | |
* @link https://www.timjensen.us/acf-get-field-alternative/ | |
* | |
* @version 1.2.5 | |
* | |
* @param integer $post_id Required. Post ID. | |
* @param array $config Required. An array that represents the structure of | |
* the custom fields. Follows the same format as the | |
* ACF export field groups array. | |
* @return array | |
*/ | |
function get_all_custom_field_meta( $post_id, array $config ) { | |
$results = array(); | |
foreach ( $config as $field ) { | |
if ( empty( $field['name'] ) ) { | |
continue; | |
} | |
$meta_key = $field['name']; | |
if ( isset( $field['meta_key_prefix'] ) ) { | |
$meta_key = $field['meta_key_prefix'] . $meta_key; | |
} | |
$field_value = get_post_meta( $post_id, $meta_key, true ); | |
if ( isset( $field['layouts'] ) ) { // We're dealing with flexible content layouts. | |
if ( empty( $field_value ) ) { | |
continue; | |
} | |
// Build a keyed array of possible layout types. | |
$layout_types = []; | |
foreach ( $field['layouts'] as $key => $layout_type ) { | |
$layout_types[ $layout_type['name'] ] = $layout_type; | |
} | |
foreach ( $field_value as $key => $current_layout_type ) { | |
$new_config = $layout_types[ $current_layout_type ]['sub_fields']; | |
if ( empty( $new_config ) ) { | |
continue; | |
} | |
foreach ( $new_config as &$field_config ) { | |
$field_config['meta_key_prefix'] = $meta_key . "_{$key}_"; | |
} | |
$results[ $field['name'] ][] = array_merge( | |
[ | |
'acf_fc_layout' => $current_layout_type, | |
], | |
get_all_custom_field_meta( $post_id, $new_config ) | |
); | |
} | |
} elseif ( isset( $field['sub_fields'] ) ) { // We're dealing with repeater fields. | |
if ( empty( $field_value ) ) { | |
continue; | |
} | |
for ( $i = 0; $i < $field_value; $i ++ ) { | |
$new_config = $field['sub_fields']; | |
if ( empty( $new_config ) ) { | |
continue; | |
} | |
foreach ( $new_config as &$field_config ) { | |
$field_config['meta_key_prefix'] = $meta_key . "_{$i}_"; | |
} | |
$results[ $field['name'] ][] = get_all_custom_field_meta( $post_id, $new_config ); | |
} | |
} else { | |
$results[ $field['name'] ] = $field_value; | |
} // End if(). | |
} // End foreach(). | |
return $results; | |
} |
How to Implement get_all_custom_field_meta()
The function get_all_custom_field_meta()
has two required parameters: the post ID, and a config array. The config array must have the same structure as the ACF code for exporting/importing field groups. There are two ways to build the config array — one manual and the other automated. I recommend the automated approach, which leverages ACF Pro’s Local JSON feature.
1) Manual way: Copy the fields
array from ACF’s field group export or condense it down like this. Either will work.
2) Automated way: Once you set up ACF Pro’s Local JSON feature you never have to remember to update the config. Just include the entire field group configuration JSON (which is always the latest verstion), decode it to an array, and pass the array to get_all_custom_field_meta()
. This is what it looks like:
<?php | |
// Replace with the name of your field group JSON. | |
$field_group_json = 'group_59e226a200966.json'; | |
$field_group_array = json_decode( file_get_contents( get_stylesheet_directory() . "/acf-json/{$field_group_json}" ), true ); | |
// Omit this line when using the Field Group Values package/plugin. | |
$config = $field_group_array['fields']; | |
$meta_data = get_all_custom_field_meta( get_the_ID(), $config ); |
And here is an example array that is returned by calling get_all_custom_field_meta( get_the_ID(), $config )
:
<?php | |
array( | |
'prefix_subtitle' => 'This is the subtitle', | |
'prefix_content_row' => array( | |
array( | |
'css_classes' => 'extra-class row-1', | |
'background_color' => '1', | |
'columns' => array( | |
array( | |
'column' => 'Row 1 column 1', | |
), | |
array( | |
'column' => 'Row 1 column 2', | |
), | |
), | |
), | |
array( | |
'css_classes' => 'extra-class row-2', | |
'background_color' => '3', | |
'columns' => array( | |
array( | |
'column' => 'Row 2 column 1', | |
), | |
array( | |
'column' => 'Row 2 column 2', | |
), | |
array( | |
'column' => 'Row 2 column 3', | |
), | |
), | |
), | |
), | |
); |
It’s important to note that the returned values are raw meta values as opposed to formatted values that can be returned by get_field()
.
Hurray for DRY Code!
Yes, I think there is a better way than relying on get_field()
or continually rewriting functions to retrieve ACF repeater and flexible content field values. The function get_all_custom_field_meta()
is flexible, meaning your code stays DRY (Don’t Repeat Yourself), plus it returns all of the post meta values in a nice multidimensional array.
Oh, and it’s fast too. I ran some basic performance tests and get_all_custom_field_meta()
was 50-100 times faster than get_field()
in the example provided here (and 300+ times faster in other tests that I ran). The relative performance gap widens (gets better) as the config array increases in complexity because the function is doing far less work than get_field()
.
Give it a whirl!
Awesome, I’m gonna give it a go soon!
Great, Gijs! Let us know how it goes.
Thank you for posting this Tim. As a newbie developer (following along on KnowTheCode.io), I’m going to bookmark this post and refer back to it when I get to the point in my learning when I start learning about `post_meta` and `get_post_meta`. I’ve seen/read complaints by others about the poor performance of ACF’s extended functionality. Up until now, I’ve not seen an alternative work around that relies on PHP and the WordPress API.
You’re welcome, Robert. ACF functions are extremely powerful and convenient but they do come at a cost, as you know. Hopefully this code will come in handy for you at some point.
Hello, could you clarify if this method still applies for ACF 5 ? I know there were many integrations but I didn’t see in your article what version this testing is done on. Thanks for your post!
Hi Kurt, thanks for your comment. I have tested this approach with ACF Pro version 5+ and it works great. Let me know how it goes for you.
Just looped back around to see you’ve updated to use ACF-JSON, this a GREAT addition! The biggest downfall of this was updating the config as things changed, but now there is no need. Super useful man, thanks again!
Thanks, Mike. I’m sure you’ll find ways to make it even more useful, so feel free to contribute!
Great Tutorial and works great. I’m wondering, if there’s a way to set the field_group_json globally so that I don’t have to add it to every template that I’m using. I’m using the underscore theme and if I want to access global variables in the template parts, it doesn’t work.
Any Idea?
If I’m understanding you correctly, you should be able to add a small function to functions.php that will handle this. Here’s something to get you started: https://gist.github.com/timothyjensen/b20040fd28130672c734527338081286.
Not exactly what I meant, but also very useful.
I’ve got another problem. Using flexible content works perfectly fine with this method, but if I use a clone field in the flexible content, I don’t get any data from that. Any idea how to fix that?
This method also doesn’t work, when you group Elements.
Thanks for reporting these problems. Can you please open up an issue for each of these (and any other) limitations that you have found at https://github.com/timothyjensen/acf-field-group-values? That way we can keep track of their status and make sure to correct them in due time.
Thanks for opening the issue on the repository. I’ve now added support for clone fields and group fields.
Hi Tim & thanks for this! I’m excited to get it working. Unfortunately I’m getting an error when I try to call the function:
Catchable fatal error: Argument 1 passed to TimJensen\ACF\Field_Group_Values::get_field_value() must be an instance of TimJensen\ACF\string, string given, called in C:\Users\Michelle\Documents\Websites\sitename.dev\wp-content\plugins\acf-field-group-values-master\src\Field_Group_Values.php on line 78 and defined in C:\Users\Michelle\Documents\Websites\sitename.dev\wp-content\plugins\acf-field-group-values-master\src\Field_Group_Values.php on line 155
Using get_field() for this same field works correctly (it’s a WYSIWYG field). The $config array is being generated correctly – I used the local JSON method. Any ideas of what could be causing this?
thanks! Michelle
Hi Michelle,
Can you share a Gist (https://gist.github.com/) showing how you’re implementing the code?
Thanks Tim! Here’s a gist. At the top is my json, below is code from my page.php file with comments to explain. Your original code is running as a plugin (I downloaded as a ZIP yesterday from Github). Thanks for your help. https://gist.github.com/FriendlyWP/f418da67f59ad83f823cb5da055bd8e2
Ah, I see the issue. Scalar type hints are supported as of PHP 7, so you’ll need to either switch to PHP 7+, or use the function in this post as opposed to the plugin on GitHub.
I neglected to add the PHP requirement to the README. Sorry for the inconvenience. Please let me know if you get it working.
That did it! Thanks so much! 🙂
Hi again Tim – just moved to PHP 7 and downloaded the latest version, thanks! I’m now trying to get this working on fields saved to taxonomy term pages. I am able to retrieve values using the usual taxonomy+ID method of passing the faux “post ID” for taxonomy terms, ie:
get_field(‘color’, ‘category_17’);
Which gives me a #hexvalue. But when I try passing ‘category_17’ as the postID here:
$acf_post_meta = get_all_custom_field_meta( $postID, $config);
var_dump($acf_post_meta);
I get an array with 1 element, color => false. I’ve confimed my $config is pulling everything from the taxonomy JSON.
Any ideas? Thanks!
Hi Michelle,
I haven’t built support for taxonomy terms, but thanks for the idea!
Honestly, unless you’re using repeater or flexible content fields it might be simpler to use get_term_meta(). I only use get_all_custom_field_meta() when working with more complex fields (repeater/flex).
Hi Michelle,
I’ve just updated the plugin to version 2.1.0 with support for term meta. You will need to supply the term ID in the form of ‘term_17’. I hope this helps!
Hello Tim,
This is fantastic! One question though– this would prevent the site from breaking and not displaying metadata if ACF is removed as a plugin, but if ACF is removed, then no new metadata in ACF fields could be saved to the database. Right? Does ACF use custom functions for updating the database?
I was hoping for a way to be able to use ACF just for field creation, and to remove it once a website is deployed.
Thanks.
Hi Bogdan, if you removed ACF the site would still display all the existing metadata on the front end; you just wouldn’t have a convenient way to add meta on the backend.
Hi Tim!
I am looking to implement this into my current build. I am using acf to create templates for pages and sections within posts. I would be needing to include several export files. How would you suggest that I use the lookup function that you referenced in a pervious answer.
Thank you much!
You can create a helper function like this, https://gist.github.com/timothyjensen/b20040fd28130672c734527338081286, so that you are able to get the field configurations by name. Does that help?
Tim,
Thank you for getting back so quickly! This does. I saw this from an older question. I have some configuring on my end to see where I will be putting something like this in.
Thanks again!
First. Thank you for sharing this plugin! It is brilliant and working as expected to help with my performance, but I am struggling with the data from the multidimensional arrays. The data is there ( checked with the echo var_dump($cta) —-). How exactly do we display the $cta repeaters? It’s probably so obvious but I’m just not seeing it.
You will need to loop through your repeater data using a foreach loop (or similar). Is that what you mean?
I figured it out. It was a basic … for each…
Thanks for the reply! Any tips on working with flexible fields. Because that’s got me stuck.
The old way I would
while ( have_rows( ‘blocks’ )) : the_row(); ?
$bid = get_sub_field( ‘block’ );
then I would
if( have_rows(‘modular_content’, $bid ) )
to determine the layout I would load.
else if( get_row_layout($bid) == ‘ctas’ ) { ?>
<?php include('ctas.php');
With the new method, I can't figure how to use the id to determine the ACF layout
Flexible content fields are best accessed using a switch statement. I use the same approach as Bill Erickson. Checkout these two posts for more information and some code snippets:
https://www.billerickson.net/advanced-custom-fields-frontend-dependency/
https://www.billerickson.net/landing-pages-with-acf-flexible-content/#accessing-the-data
There is another approach you can use to access the entire array of field objects. This functions works like this: $fields = get_field_objects( $post_id ); Effectively it gives you all the saved fields on a particular post as an array. You then don’t need to continually use get_field or get_sub_field, but use the array as normal. The sparse docs on it are on the acf website here: get_field_objects – I’ve used this a few times with good results.