author.php with ACF and CPTs

REWORKED APPROACH

The issue with the original answer is that although we pass post ID’s to post__in parameter, being on the author page, the main query removes all posts that does not belong to the author. We can pass an empty string to author_name via the pre_get_posts filter, but that inherintly breaks the query object, which is a no-no.

This calls for a new approach, and this is what we will do:

  • We will let the main query run as normal, that is, it will return all posts from the default post type post which was authored by the current author being viewed. That will automatically take care of issue 2. Issue one with displaying the custom fields is handled in the original approach.

    We can use pre_get_posts here if we need to append custom post types to the original main query, but from what I understand, you only need normal posts, so this would not be necessary

  • To sort the second issue where we need only posts from the post type projetcs where the author being viewed is mentioned, we will rather alter the SQL query generated than adding our posts via pre_get_posts

    For this, we will make use of the posts_where filter. We will run a custom query to return all posts’ id’s where the author is mentioned from projects, we will then take those id’s, and alter the SQL query’s WHERE clause to also getthose posts.

    We will still use our custom trigger to trigger the custom query to also alter the generated SQL for the meta_query.

So, for the code, you can remove all code I posted in my original approach, and replace it with our new filter. (Just again, we need PHP 5.4)

add_filter( 'posts_where', 'posts_where_credited', 10, 2 ); 
function posts_where_credited( $where, \WP_Query $q )
{
    // Lets first check our trigger and change the SQL if needed
    if ( true === $q->get( 'wpse_trigger' ) )
        $where = str_replace(
            "meta_key = 'project_credits_%_user",
            "meta_key LIKE 'project_credits_%_user",
            $where
        );

    // Make sure that we target the main query, front end on author pages
    if (    !is_admin()
         && $q->is_main_query()
         && $q->is_author()
    ) {
        // Get the current author
        $author_name = sanitize_title_for_query( $q->get( 'author_name' ) );
        // Just to make sure we actually have a valid uathor name, if not, bail
        if ( !$author_name )
            return $where;

        $author = get_user_by( 
            'slug',
            $author_name
        );
        $author_id = absint( $author->ID );

        // Get the posts in which the author is mentioned from our post type
        $args = [
            'wpse_trigger'           => true, // Our custom trigger
            'post_type'              => 'projects',
            'posts_per_page'         => -1,
            'fields'                 => 'ids',
            'suppress_filters'       => false,
            'cache_results'          => false,
            'update_post_term_cache' => false,
            'update_post_meta_cache' => false,
            'meta_query'             => [
                [
                    'key'   => 'project_credits_%_user',
                    'value' => $author_id
                ]
            ]
        ];
        $post_ids = get_posts( $args );
        // Make sure we have id's, else bail
        if ( !$post_ids )
            return $where;

        $post_ids = implode( ',', array_map( 'absint', $post_ids ) );

        // We have id's, lets adjust the SQL WHERE clauses
        global $wpdb;

        $where .= " OR ( $wpdb->posts.ID IN ( $post_ids ) ) ";
    }
    return $where;
}

ORIGINAL APPROACH

You are very close with your approaches, just a glitch here and there. Lets break all of this down

displays data from Advanced Custom Fields on the user’s Profile Page

This should be straight forward. Unfortunately I have never worked with ACF, so I do not know how the data is stored. However, in short, all you need is the current ID of the author archive page being viewed. You already have that ID available (+1 for using the queried object).

Alternatively, and a much more reliable way to get the queried object will be to make use of the main query object stored in $GLOBALS['wp_the_query']. $GLOBALS['wp_the_query'] is much more reliable than $GLOBALS['wp_query'] as the latter can be altered by crappy functions like query_posts. So in short, in your author archive page, you can use the following

if( !isset($whose_ID) ) {
    $whose_ID = absint( $GLOBALS['wp_the_query']->queried_object_id );
}

You can then use $whose_ID in your get_field() functions to return the ACF data. From your code, it seems that the key has the user prefix, so

$search="user_" . $whose_ID;

should do.

displays author’s posts all posts that the user has authored

It is here that all your approaches lack a small thing or two or just need some refinement. Your first approach and last approach should have been merged into one. One thing that we need to keep in mind here is that WP_Query does not natively support the type of query that you are after, so we cannot natively do this in one query, so we will need more than one query. This is turn needs us to be clever as this can get expensive and even break the bank.

What we will be doing here to solve this section is:

  • Use get_posts to get all the post ID’s from the post type post that the author has written. This can be an expensive overhead, so we need to be very clever here. get_posts are faster than WP_Query as it legally breaks paging, but we need to make it even faster as we do not need to break the bank.

    What we will do is, to make it super fast, we will be passing 'fields' => 'ids' to our get_posts query args. This will tell the query to only return an array of post id’s. This makes the query very fast as we do not return any other postdata. We can also make is even faster by telling the query not to cache post terms and meta data. This is all irrelevant stuff that we do not need

So lets look at this query (PLEASE NOTE: All code is untested and requires PHP 5.4+)

$author_name = sanitize_title_for_query( $q->query['author_name'] );

$args_1 = [
    'author_name'            => $author_name, // Get from referenced query
    'posts_per_page'         => -1, // Get all posts
    'cache_results'          => false, // Do not update post cache
    'update_post_meta_cache' => false, // Do not update post meta cache
    'update_post_term_cache' => false, // Do not cache post terms
];
$post_ids_1 = get_posts( $args_1 );

$post_ids_1 will now hold an array of post ids from posts that the author been currently viewed authored from the default post type post

  • all projects (CPT) in which the user is Credited (assigned via an ACF Field for the CPT)

For this section, we will follow the same exact process as above, so I’m not going to go into detail here. Lets look at the code

// This section is also used by WP_Query to get user id
$author = get_user_by( 
    'slug', 
    $author_name 
);
$author_id = absint( $author->ID );

$args_2 = [
    'post_type'              => 'projects',
    'posts_per_page'         => -1, // Get all posts
    'cache_results'          => false, // Do not update post cache
    'update_post_meta_cache' => false, // Do not update post meta cache
    'update_post_term_cache' => false, // Do not cache post terms
    'meta_query' => [
        [
            'key'   => 'project_credits_%_user',
            'value' => $author_id
        ]
    ]
];
$post_ids_2 = get_posts( $args_2 );

You now have all the post ids where the author was mentioned from post type projects

Now that we have all relevant id’s, it is time to put everything together in our pre_get_posts action

add_action( 'pre_get_posts', function ( $q )
{
    // Remove action
    remove_action( current_action(), __FUNCTION__ );

    // Only target the main query, front end only on author archive pages
    if (    !is_admin()
         && $q->is_main_query()
         && $q->is_author()
    ) {
        // Now we can run our custom queries to get post ID's

        $author_name = sanitize_title_for_query( $q->query['author_name'] );

        $args_1 = [
            'author_name'            => $author_name, // Get from referenced query
            'posts_per_page'         => -1, // Get all posts
            'cache_results'          => false, // Do not update post cache
            'update_post_meta_cache' => false, // Do not update post meta cache
            'update_post_term_cache' => false, // Do not cache post terms
        ];
        $post_ids_1 = get_posts( $args_1 );

        // This section is also used by WP_Query to get user id
        $author = get_user_by( 
            'slug', 
            $author_name 
        );
        $author_id = absint( $author->ID );

        $args_2 = [
            'post_type'              => 'projects',
            'posts_per_page'         => -1, // Get all posts
            'cache_results'          => false, // Do not update post cache
            'update_post_meta_cache' => false, // Do not update post meta cache
            'update_post_term_cache' => false, // Do not cache post terms
            'meta_query' => [
                [
                    'key'   => 'project_credits_%_user',
                    'value' => $author_id
                ]
            ]
        ];
        $post_ids_2 = get_posts( $args_2 );

        // It is now just a matter of merging the two arrays of ids
        $combined = array_unique( 
            array_merge( 
                $post_ids_1, 
                $post_ids_2 
            ) 
        );

        // We need to make 100% sure we have id's, otherwise post__in will return all posts
        if ( !$combined )
            return;

        // Lets build the query
        $q->set( 'post_type', ['post', 'projects'] );
        $q->set( 'post__in',  $combined            );
    }
});

You can display your posts normally on your author archive page with the default loop. Just a note, according to the site’s syntax highlighter, there seems to be a syntax error on the code in your author.php

EDIT

I had a look at your edit, and there are a couple of issues, but it does not necessarily means this will solve the issue

  • You do not need to call wp_reset_postdata() and wp_reset_query(). The latter is only used with query_posts() which you should never ever use

  • get_posts() does accept to be altered by filters, but this is not default behavior. get_posts() passes 'suppress_filters' => true to WP_Query by default which suppress filters altering the query. You can override this by simply passing 'suppress_filters' => false to you get_posts arguments. This will allow filters to alter the query

  • By default, any of the filters in WP_Query will alter all instances of WP_Query (which includes the main query). It is always a good idea to use some kind of trigger to trigger a filter as to target only the relevant query. I would definitely do this with your posts_where filter.

    What we can do is, lets call our trigger wpse_trigger. We can pass 'wpse_trigger' => true to our query argument of the query we would want to target with our filter. All we need to do now is to check inside our filter if our trigger is set and if it is set to true. Remember, the current query is passed by reference to the filter as second parameter.

    Lets look at the code

    function posts_where_credited( $where, \WP_Query $q ) 
    {
        // Lets remove the filter
        remove_filter( current_filter(), __FUNCTION__ );
    
        // Lets see if our trigger is set and if the value is true, if not, bail
        if ( true !== $q->get( 'wpse_trigger' ) )
            return $where
    
        // Our trigger is set and true, lets alter this query
        $where = str_replace(
            "meta_key = 'project_credits_%_user", 
            "meta_key LIKE 'project_credits_%_user", 
            $where
        );
    
        return $where;
    }
    add_filter( 'posts_where' , 'posts_where_credited', 10, 2 );
    

    Your query arguments for your second get_posts instance where you make the meta query can now look something like this:

    $args_2 = [
        'wpse_trigger'    => true,
        'suppress_filter' => false,
        // Rest of your query args
    ];
    
  • I do not know if your filter actually works correctly with the meta_key LIKE, but you can have a look at this post for some alternatives if it actually is query two that fails

What is quite strange is that everything works for author 1, but not for ny other author. Here is a few things you can check

  • Keep your WP_Query instance for the second query inside your pre_get_posts action. Then after that query (right after $post_type_projects_ids = new WP_Query();), do var_dump( $post_type_projects_ids->request ); and load the author page. This will print the SQL query on top of your author page. Check if resultant SQL query matches on all other author pages and that of author 1. If they differ, you have an issue somewhere in a plugin or in the theme with a bad filter (like posts_where or posts_clauses), action (bad pre_get_posts) or an issue with capabilities of your custom post type. Note, you have swith to WP_Query as this will not work with get_posts

  • If the SQL queries checks out, then do var_dump( $post_type_projects_ids->posts ); and check if you actually have posts that is returned from the query. This will just be an array with ids if there are posts

  • If the above checks out, go to your author page, and anywhere on that page, add var_dump( $wp_query->request );. This will print the SQL query generated by the main query. Again, because author 1 works, compare that SQL query with the SQL query of other author pages. Except for post and author id’s, the queries should match. If not, refer to bullet point one for debugging

Leave a Comment