Using Advanced Custom Fields without frontend dependency

The performance issues in ACF have been resolved. You can use either ACF or core WordPress functions for accessing the data with the same result performance-wise.

Advanced Custom Fields is one of the most popular methods of creating metaboxes. It provides a simple visual interface for creating the metaboxes, and the resulting metaboxes look great and are easy for clients to edit.

ACF also includes a bunch of functions you can use in your theme to retrieve the values of the meta fields. But I recommend you don’t use these.

For some fields like images, this will introduce additional database queries. See the comment below where switching from ACF functions to the WP core function decreased database queries from 146 to 22!

There’s is no reason to use an ACF function like get_field() when you can use a WordPress core function like get_post_meta(). Using these ACF functions introduces what I’m calling “frontend dependency”.

If you use an ACF field and the ACF plugin is removed or deactivated (because clients…), visitors to the website will see the white screen of death instead of the website. If you go to the bother of wrapping all the ACF functions in function_exists(), the site will load but none of your metadata will be shown.

But the metadata still exists in the WordPress database. If you had used WordPress functions, there would be no changes to the frontend of the website. On the backend the metaboxes would be gone so they couldn’t edit those fields (unless they use the WP built-in Custom Fields metabox), but this is a much less serious issue than a broken website.

I’ve also found that ACF can cause performance issues on the frontend, even if you’re not using their fields. You can add this code to prevent the plugin from loading on the frontend.

This works fine for simple fields, but it might not be clear how the more complex fields are stored in the database. I’ll walk you through some examples.

Repeating Fields

On this page, as many videos can be included as they like (or none at all). I’m using a repeater field which includes a Title, Video URL and Thumbnail (sample fields).

repeating-videos

In your theme, if you look at the meta field for the repeater field ( get_post_meta( get_the_ID(), 'be_attorney_video', true ); ), it will give you the number of items within that field. You can then loop through and access all of them. Here’s how I’m assembling those videos:

<?php
$videos = get_post_meta( get_the_ID(), 'be_attorney_video', true );
if( $videos ) {
for( $i = 0; $i < $videos; $i++ ) {
$title = esc_html( get_post_meta( get_the_ID(), 'be_attorney_video_' . $i . '_title', true ) );
$video = esc_url( get_post_meta( get_the_ID(), 'be_attorney_video_' . $i . '_video', true ) );
$thumbnail = (int) get_post_meta( get_the_ID(), 'be_attorney_video_' . $i . '_thumbnail', true );
// Thumbnail field returns image ID, so grab image. If none provided, use default image
$thumbnail = $thumbnail ? wp_get_attachment_image( $thumbnail, 'be_video' ) : '<img src="' . get_stylesheet_directory_uri() . '/images/default-video.png" />';
// Displayed in two columns, so using column classes
$class = 0 == $i || 0 == $i % 2 ? 'one-half first' : 'one-half';
// Build the video box
echo '<div class="' . $class . '"><a href="' . $video . '">' . $thumbnail . '</a>' . $title . '</div>';
}
}
view raw functions.php hosted with ❤ by GitHub

This snippet also shows you how to use ACF image fields. They store the image ID, so you can get the actual image markup using wp_get_attachment_image( $image_id, 'image_size' );.

Flexible Content

In my last post I described setting up the HTML markup for Full Width Landing Pages. This section is a bit like “Part 2” of that post, since almost every page I set up in that way I’m using ACF’s Flexible Content field to populate the content area.

outboundengine

Flexible Content allows you to create different layouts, which are collections of fields. For instance, a Testimonial layout might have fields for Quote, Person’s Name, and a Photo. A website editor can then create a page by assembling these layouts in whatever order they like, and use them as many times as they like.

Take a look at the OutboundEngine homepage. The header contains a menu and call to action (graphic, text and form). The footer is the dark blue part at the bottom starting with “Sign up and watch your business grow”. Everything in between is the flexible content area. This page is made up of the following layouts: Features, Small Quote, Full Width Content, and Full Width Content.

For this example, my Flexible Content field has a name (meta_key) of ‘be_content’. I have two layouts: full width content (fields: title and content) and content with image (fields: title, content, image). Assuming you set up your page template like I did in my Full Width Landing Pages tutorial, this would be your template-landing.php file:

<?php
/**
* Your Child Theme
*
* Template Name: Landing
*/
/**
* Flexible Content
*
*/
function be_landing_page_content() {
$rows = get_post_meta( get_the_ID(), 'be_content', true );
foreach( (array) $rows as $count => $row ) {
switch( $row ) {
// Full Width Content layout
case 'full_width_content':
$title = esc_html( get_post_meta( get_the_ID(), 'be_content_' . $count . '_title', true ) );
$content = get_post_meta( get_the_ID(), 'be_content_' . $count . '_content', true );
echo '<div class="flex-content full-width-content"><div class="wrap">';
if( $title )
echo '<h2>' . $title . '</h2>';
if( $content )
echo '<div class="section-content">' . apply_filters( 'the_content', $content ) . '</div>';
echo '</div></div>';
break;
// Content with Image layout
case 'content_with_image':
$title = esc_html( get_post_meta( get_the_ID(), 'be_content_' . $count . '_title', true ) );
$content = get_post_meta( get_the_ID(), 'be_content_' . $count . '_content', true );
$image = get_post_meta( get_the_ID(), 'be_content_' . $count . '_image', true );
echo '<div class="flex-content content-with-image"><div class="wrap">';
if( $title )
echo '<h2>' . $title . '</h2>';
if( $image )
echo '<div class="section-image">' . wp_get_attachment_image( $image, 'medium' ) . '</div>';
if( $content )
echo '<div class="section-content">' . apply_filters( 'the_content', $content ) . '</div>';
echo '</div></div>';
break;
}
}
}
add_action( 'be_content_area', 'be_landing_page_content' );
// Remove 'site-inner' from structural wrap
add_theme_support( 'genesis-structural-wraps', array( 'header', 'footer-widgets', 'footer' ) );
// Build the page
get_header();
do_action( 'be_content_area' );
get_footer();

The ‘be_content’ meta field will be an array of layouts used on this page, in order. The fields in each layout will be stored based on that order. So if you have a field named ‘title’ and it’s in the first layout on the page, it will be stored as ‘be_content_0_title’ (counting starts at 0, so the first one is 0).

Options Pages

Metaboxes you add to options pages will store data as options instead of post meta. It will be the meta_key you specify, prefixed with ‘options_’. So, if you had an options page with a Call to Action box (sample fields), you would access those fields like this:

<?php
function be_call_to_action() {
$title = esc_html( get_option( 'options_be_cta_title' ) );
$button_text = esc_html( get_option( 'options_be_cta_button_text' ) );
$button_url = esc_url( get_option( 'options_be_cta_button_url' ) );
if( $title && $button_text && $button_url )
echo '<div class="call-to-action"><div class="wrap"><p>' . $title . '</p><p><a href="' . $button_url . '" class="button">' . $button_text . '</a></p></div></div>';
}
add_action( 'genesis_before_footer', 'be_call_to_action' );
view raw functions.php hosted with ❤ by GitHub

Term Metadata

Let’s say you added a Subtitle field to categories (sample fields). ACF stores these as options as well, with the following format: [taxonomy]_[term_id]_[field name]

So with a field name of ‘be_subtitle’, you’d use the following to access it on a category archive page:

<?php
/**
* Category Subtitle
*
*/
function be_category_subtitle() {
// Make sure this is a category archive
if( ! is_category() )
return;
$category = get_term_by( 'slug', get_query_var( 'category_name' ), 'category' );
$subtitle = esc_html( get_option( 'category_' . $category->term_id . '_be_subtitle' ) );
if( $subtitle )
echo '<p class="subtitle">' . $subtitle . '</p>';
}
add_action( 'genesis_before_loop', 'be_category_subtitle' );
view raw archive.php hosted with ❤ by GitHub

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson

Comments

  1. hans says

    first, thanks for the article, great info!
    BUT what is bad in just including the ACF plugin in my theme so the customer doesn’t need to even install it?
    http://www.advancedcustomfields.com/resources/including-acf-in-a-plugin-theme/
    plus if i export the fields in PHP and put them in my functions.php (or use json) i should have a rock solid solution(?).
    i took a look at CMB and … i think ACF gives me WAY more optionsand it just looks sweet. i build my gallerys with the gallery field, have flexible fields with tons of options, repeater fields … i do not need drupal šŸ˜‰

    • Bill Erickson says

      As far as I know, if you include ACF in your theme it won’t receive any updates. Given the size and complexity of ACF, it seems to get updated all the time to fix newly found issues.

    • Matt Whiteley says

      Hey Hans,

      I had the same question that Bill addressed. The main downfall to doing it this way is that it no longer gets automatic updates if it is included in the theme, so unless you are managing the site and handle the updates you can end up with un-patched bugs.

      I do love ACF though and typically just tell clients not to touch it (works most of the time…). I now use ACF is 100% of my client projects.

      Cheers,

      Matt

      • Bill Erickson says

        Yes, I recommend you just use the plugin and tell the client not to disable it.

        This post isn’t anti-ACF, I still use it on many projects as well. It just points out that there’s no real need to use ACF-specific functions in your theme when you can use the WordPress equivalent.

        • Matt Whiteley says

          Absolutely – I should note that I did take your advice and no longer use the get_field, the_field, the_sub_field, etc…I use the built-in WordPress functions (ie. get_post_meta) per your suggestion.

          Definitely wasn’t saying this post was anti-ACF! I’m there with you.

        • hans says

          thanks a lot bill this is a very good point.
          so i think my steps have to be:
          buying a “personal licence” of $25 for each customer who is buying my premium theme and sending the key to the customer
          – installing and activating the ACF plugin automatically when the customer activates my premium theme (i do this with “tgmpa”)
          – the customer has to enter the licence key to enable automatic update function on ACF

          this is a little drawback because when i want to sell each premium theme for e. g. $50 i have to give 50% away … hmmm.

          • Bill Erickson says

            Why not by the professional license once and use that key on all sites? That’s what I use for clients. If they need support they can either go to you or purchase their own personal license.

      • hans says

        hi matt,
        yes! i use ACF in every single project since years, never had a problem, just pure awesomeness everywhere šŸ™‚

  2. hans says

    hi bill,
    thanks for your quick answer!
    i just have not seen this part of the ACF 5 pro licence that says:
    – “Updates for Unlimited Sites”
    my fault šŸ˜‰

  3. Queequeg says

    Hi! Thank you for your post, it is very helpful to me. I work on a site that uses ACF and I needed to delete some of these fields, but I still need their values. Now I’m facing some problems, because of the use of get_field().. I’ ve got a repeater field with three subfields, one of them a taxonomy select field. I would like to retrieve values from a subfield “name”, where the equivalent taxonomy subfield “type” has a specific selection. Before deleting the fields I had something like this:
    $items = get_field(“items”, $id);
    foreach($items as $t) {
    if ($t[‘type’] == selection5) {
    return $t[‘name’];}
    }
    If I use get_post_meta() I only get the number of items, or nothing. Is there a correct way to replace the above function? Thanks in advance!

    • Bill Erickson says

      In your theme file place this: print_r( get_post_meta( get_the_ID() ) );

      That will show you all the meta attached to the post. Go through it, find what you’re looking for, and you’ll see the proper key to use.

      • Queequeg says

        Thank you for your quick answer, your help was really precious! I wasn’t using the correct key.
        It works great now!

  4. reader says

    could you make the text harder to read here? light gray text on this bg? wow not cool..
    Why not try say p= 3c3c3c at least? Im not even that old but god forbid if I were like 50 or something..
    Im 19 and I am not color blind or anything but still??? is it me? No its you..
    Plus try a larger font size say even 16px I mean this is terrible.. your text on your bg on this site i mean other than that you are a very nice man with much knowledge..
    Beth

  5. Dylan says

    Thanks for writing this up Bill. Your approach works great, the only downside I can see at the moment is that changes to ACF fields don’t display in the post preview unless you make changes to a native wp field as well. Fields preview correctly when using the ACF functions. The plugin uses the get_post_id function to return the preview post ID if the post is being previewed. Would you just implement a simplified version of this in your theme?

    • Bill Erickson says

      Yes, if you wanted the updated metadata to appear on previews, write a helper function that uses the exact same logic you linked to.

  6. Dmitriy says

    Thank you so much for this. Wish I saw it before spending hours troubleshooting. I have WP GeoDirectory alongside ACF and they don’t work well together – all GeoDir pages fail to retrieve any ACF values. Falling back on the native WP functions did the trick! Cheers!

  7. Patrick Fortino says

    Bill, Thanks for this very clear tutorial. It helped me get a much better understanding of how custom fields work.

    There is an error in your code for flexible content:
    // Content with Image layout
    case ‘content_with_image’;

    The case should end with a : not a ;
    I was getting this error: Warning: Invalid argument supplied for foreach() in /…

    In order to fix the foreach invalid argument error, I had to change the code to this:
    foreach ((array) $rows as $count => $row ) {

    I assume the code works for you, so not sure why Iā€™m getting the error, but the code above fixes the error message.

    • Bill Erickson says

      Thanks! I’ve updated the code. That’s what happens when you write the code in a Gist without testing it šŸ™‚

      • Patrick Fortino says

        FYI for anyone else who is getting this error with the Flexible Content code:
        Warning: Invalid argument supplied for foreach() in /ā€¦

        After further testing, I found that it was my php version that was causing the error. Changing to php 5.6.10 or higher fixed the error.

        • Sarah Hills says

          I also had problems with the foreach loop for the Flexible Content code.

          As an alternative, I used: “for( $count = 0; $count < $rows; $count++ )“

  8. Uri says

    Hi BIll.
    Experimenting with this very useful ‘best-practice’ of not having a front-end dependency and struggling with one of my fields in ACF . It’s an image type field and when I try to echo it out directly to a style tag I am getting an id instead of the image URL?

    Example Code:
    $program_background_image = get_post_meta( get_the_ID(), ‘cii_programs_’ . $count . ‘_program_background_image’, true );

    and then trying to use it like this:
    echo ”;

    Any ideas how to pull image fields using get_post_meta

    • Bill Erickson says

      Advanced Custom Fields stores the Image ID, which much more useful since you can use it to generate an image of any size. Use the wp_get_attachment_image( $image_id, $size ) function. If you wanted to output the ‘full’ image you uploaded, do this:

      $image_id = get_post_meta( get_the_ID(), 'cii_programs_' . $count . '_program_background_image', true );
      echo wp_get_attachment_image( $image_id, 'full' );

      If there is a specific image size you want use, specify that instead of ‘full’.

  9. Luyen Dao says

    It seems like, more than any other plugin once you go with ACF, you really have to go all in.

    Another way to approach this, if you have full control over the theme development is to make sure that the plugin cannot be removed (easily), beyond what you’ve stated in your article are there any performance gains from using the native functions to get meta data?

    Cheers

    • Bill Erickson says

      I haven’t done any performance testing, but I do know the ACF functions return a lot more information so they’re probably doing more than one query. For instance, if you `get_field()` for an image field, you end up with a large array of data.

      But if you `get_post_meta()` that field, you get the image ID. That tells me that ACF is using `get_post_meta()` to get the ID, then running additional functions like `wp_get_attachment_image_src()` for each image size to get all the additional information.

      Again, I haven’t done performance testing to see the impact of this, but I like using the core functions and then specifically requesting the information I need rather than getting a dump of all possible information related to that meta field.

      If you do use `get_field()`, the third parameter is $format. If you set that to `false` you should get the same output as `get_post_meta()`

  10. Carles says

    I’m astounded. Seriously.

    I’m developing a Ski site with Genesis that makes heavy use of ACF’s features. There are several Repeaters and Flexible Contents for every single page. According to the Query Monitor plugin, that adds up to 18MB of memory use and 146 database queries. As a skier, I know you’ll be interested :D, beta in http://www.cameraski.com/skiresorts/baqueira-beret/
    I’m not a pro (so bear with me) and haven’t found any other article like this -and believe me, I’ve tried-, but found a lot of people defending the use of ACF’s own code over WP’s native one. So I guessed very few people (only the true pro’s) were coding like this and thought “well, this won’t be a big deal, just a very technical one”. But the huge number of queries was a serious concern. So I was eager to give it a try.
    Since I need the Flexible Content feature, I prepared a combined ACF/CMB2 solution. As I had all the ACF’s fields already made, I began by just changing the “get_field”, “get_sub_field” and “while ( have_rows() )…” code with “get_post_meta()”. Still no CMB2, no ACF update, no nothing.
    To my surprise, the memory usage has fallen from 18MB to 14.23MB and database queries from 146 to… 22! Truly shocking.
    My understanding was that your article was all about good practices and avoiding problems with clients. In the comments, you even mention that “CMB stores repeaters as serialized array”, so I thought if one wants to reduce database calls, that’s the way to follow.
    So Bill, I have two questions: do I still need CMB2? And more important, what did just happen with my queries? What’s the magic?

    Thanks for your really useful articles, they are very enlightning.

    PS.- Regarding sanitization, I’ve digged into the Codex but that wasn’t clear to me. In your code, you use “esc_html”, “int”, etc. and I’ve seen others like “esc_js” or “sanitize_text_field”. I’m not sure which one to use for every type of ACF field. Do you know of any article or post with detailed info? Thanks again.

    • Bill Erickson says

      Thanks for the performance details! I’ve updated the post above to make note of it.

      When the post first loads, an additional query runs to retrieve all of its metadata. So adding additional meta fields will not increase the number of queries (that single query still gets all of it) but will increase the memory footprint based on the size of that data.

      But for some fields, ACF runs additional queries for related information. For instance, the image field stores the image ID as post meta. But if you use get_field(), it returns a bunch of additional information including the image object and the URLs for the images at different image sizes.

      If you must use ACF’s get_field(), you should set the third parameter to ‘false’. As you can see here, get_field() accepts three parameters:
      – Field Name (required)
      – Post ID (optional, defaults to current post if not specified)
      – Format Value (optional, defaults to true)

      So if you do get_field( 'field_name', get_the_ID(), true ); you’ll get just the image ID, the same as if you used get_post_meta( get_the_ID(), 'field_name', true );

      Daniel Bachhuber has a great resource describing how to sanitize input and escape output

      • Carles says

        Nope, no more get_field() anywhere. It’s all get_post_meta(), even for the Flexible Content, based on your code and some tweaking. I must say I struggled with those a bit, as I thought the switch() you use in your code was just for your specific needs. But when I tried that, it went really smooth.

        Let me clarify something, though: in order to get a good scheme in my php code, I first declare the variables for the field (e.g. $snow_depth = get_post_meta( get_the_ID(), ‘snow_depth’, true ); Then, later in the “Snow Depth” section of the code, I take charge for the subfields, like “$snow_depth_elevation_1 = get_post_meta( get_the_ID(), ‘snow_depth_’ . $i . ‘_snow_depth_elevation_1’, true );”.

        So I started with just the fields in the first lines of the code. Out of curiosity, I reloaded the page and queries felt from 146 to 72, which is a lot. This is because ACF makes 2 queries per field/sub-field, one for its reference and one for its value.

        As for images, if I recall correctly, queries were reduced only by 2, which seems to match the reference/value queries but not the image sizes. It seems to me that ACF has already fixed that.

        Another curious issue is when going for the specific sections (snow, slopes, lifts, etc.) and their subfields. I would think that the variety of ski slopes or lifts values would reduce queries by a lot (we’re talking Repeaters in Flexible Content here), but nope, just 2 queries-reduction for everything slopes and lifts. Then, you do the same for something theoretically “smaller” or “simpler” like a simple text subfield in a simple field (not a Repeater), and you get 4 or 5 less queries.

        So all in all, just using get_post_meta() for the initial fields makes for a 50% reduction of your queries. Then it is step by step through all your subfield complexity. Sometimes you get 2 less, sometimes 5 when not expecting them.

        Thanks again.