2

I have a function, within functions.php (all code below), that adds an advertisement after x amount of paragraphs to any given single.php post. I have a custom-post-type called advertising that has a Title, Image, and URL field for the advertisement post. In any single.php post I have a checkbox to show the ad (true / false) and a number field for the author to choose how many paragraphs to skip before inserting the image advertisement.

UPDATED CODE: code below works, however I am unable to get a random post. It keeps pulling the absolute latest post without rotating to the other ones.

// http parser
function addhttp($url) {
    if (!preg_match("~^(?:f|ht)tps?://~i", $url)) {
        $url = "http://" . $url;
    }
    return $url;
}

// filter content with ad
add_filter( 'the_content', 'prefix_insert_post_ads' );
function prefix_insert_post_ads( $content ) {

    // checkbox to show ad, default true
    if ( get_field('show_advertisement') ) {
        if ( is_single() && ! is_admin() ) {
            // get post-type
            $random_ad = get_posts(array(
                'numberposts' => 1,
                'post_type' => 'advertising',
                'order' => 'rand',
                'posts_per_page'=>'1'
            ));
            // get post-type fields
            $random_ad = array_shift($random_ad);
            $link = addhttp( get_field('advertisement_link', $random_ad->ID));
            $image = get_field('upload_advertisement', $random_ad->ID);
            // get html
            $ad_code = '<a href="'.$link.'" target="_blank"><img src="'.$image.'" /></a>';
            // show ad after # paragraphs
            $show_after = get_field('advertisement_show_after');
            // return appended $content
            return prefix_insert_after_paragraph( $ad_code, $show_after, $content );
        } else {
            // do nothing
        }
    } else {
        return $content;
    }

}

// insert ad into post
function prefix_insert_after_paragraph( $insertion, $paragraph_id, $content ) {
    $closing_p = '</p>';
    $paragraphs = explode( $closing_p, $content );
    foreach ($paragraphs as $index => $paragraph) {

        if ( trim( $paragraph ) ) {
            $paragraphs[$index] .= $closing_p;
        }

        if ( $paragraph_id == $index + 1 ) {
            $paragraphs[$index] .= $insertion;
        }
    }

    return implode( '', $paragraphs );
}

EDIT: SOLVED! Changed order to orderby and the above code works.

  • 1
    you say WP_Query doesn't work, but I don't see where you've tried to use it. – Milo Feb 24 '16 at 16:20
  • because when I tried it broke the website, and I'm unsure how to add WP_Query into something usable like $ad_code. It's getting a bit over my head at this point. – Alexander Graham Feb 24 '16 at 16:21
  • Keep in mind that getting random posts is an unexpectedly expensive operation, you would be better picking a random year then querying for that, or randomly picking another search criteria in PHP – Tom J Nowell Feb 24 '16 at 16:29
2

As already pointed out, random ordering and searching is quite expensive operations to run, so lets look at how we are going to sort that issue.

Because you only need one post from your custom post type, we only need one random ID which we can pass to get_post() in order to get the desired post. Instead of getting the post randomly from the db, we will query all custom post types (or at least all the post ID's), save that into a transient, and then we can just pick a random ID from that option.

Lets look at some code: (This goes into functions.php)

function get_random_id( $post_type = '' )
{
    $q = [];

    // Make sure we have a post type set, check if it exists and sanitize
    $post_type = filter_var( $post_type, FILTER_SANITIZE_STRING );

    if ( !$post_type ) 
        return $q;

    if ( !post_type_exists( $post_type ) )
        return $q;

    // The post type exist and is valid, lets continue
    $transient_name = 'rand_ids_' . md5( $post_type );

    // Get the transient, if we have one already
    if ( false === ( $q = get_transient ( $transient_name ) ) ) {
        $args = [ 
            'post_type'      => $post_type,
            'posts_per_page' => -1,
            'fields'         => 'ids', // get only post ID's
            // Add any additional arguments
        ];
        $q = get_posts( $args );

        // Set the transient
        set_transient( $transient_name, $q, 30*DAY_IN_SECONDS );    
    } // endif get_transient

    return $q;
}       

What we have done, we have now saved all custom post type ids into a transient. This will increase performance by a huge amount. The transient is set for 30 days, so we need to flush and recreate the transient as soon as we publish a new custom post type post.

Lets use the transition_post_status action hook: (This goes into functions.php)

add_action( 'transition_post_status', function ( $new_status, $old_status, $post )
{
    // Make sure we only target our specific post type
    if ( 'advertising' !== $post->post_type )
        return;

    global $wpdb;

    // Delete the transients
    $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient%_rand_ids_%')" );
    $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient_timeout%_rand_ids_%')" );

    // Lets rebuild the transient
    get_random_id( $post->post_type );

}, 10, 3 );

All we are left with is to get a random post ID from our get_random_id() function and pass that to get_post() to get the post object

// Get the array of post ID's
$post_type_posts = get_random_id( 'advertising' );
// Make sure we have posts
if ( $post_type_posts ) {
    // Get the post object of the id first in line
    shuffle( $post_type_posts );
    $single_post = get_post( $post_type_posts[0] );
    // Display the post content
}

This way you save a lot on resources and is much faster than just simply let SQL choose a random post from the DB

Just as example, your advert injector filter can look something like the following

add_filter( 'the_content', 'prefix_insert_post_ads' );
function prefix_insert_post_ads( $content ) {

    // checkbox to show ad, default true
    if ( get_field('show_advertisement') ) {
        if (     is_single() && 
               ! is_admin() 
        ) {
            // Get the array of post ID's
            $post_type_posts = get_random_id( 'advertising' );
            // Make sure we have posts
            if ( $post_type_posts ) {
                // Get the post object of the id first in line
                shuffle( $post_type_posts );
                $random_ad = get_post( $post_type_posts[0] );

                // Display the post content
                $link = addhttp( get_field('advertisement_link', $random_ad->ID));
                $image = get_field('upload_advertisement', $random_ad->ID);
                // get html
                $ad_code = '<a href="'.$link.'" target="_blank"><img src="'.$image.'" /></a>';
                // show ad after # paragraphs
                $show_after = get_field('advertisement_show_after');
                // return appended $content
                return prefix_insert_after_paragraph( $ad_code, $show_after, $content );

            } 
        }
    } 
    return $content;
}
  • This is highly impressive, and works flawlessly as well. How are you able to check for certain that this function is less expensive (learning)? I have saved this and currently using it, though both answers were correct. Fantastic stuff. This helped me a lot with how I mentally processed this data. Thanks, Pieter! – Alexander Graham Feb 24 '16 at 20:16
  • What does the $q represent, post-type ids? or does it grab all post-type ids regardless of post-type? – Alexander Graham Feb 24 '16 at 20:39
  • 1
    $q represents an array of post id's. You can rename it to anything. I just use $q for laziness and shorthand ;-). No, $q only holds ID's from the post type you pass to the function, in this case advertising. For testing benchmark, I usually use this approach – Pieter Goosen Feb 24 '16 at 22:42
  • 1
    You're the man! I highly appreciate this, and that makes sense. All answers work but this is more efficient. Learned a lot through this. Thanks again! – Alexander Graham Feb 25 '16 at 13:36
  • 1
    My pleasure, glad you found this all usefull. Good luck on your project ;-) – Pieter Goosen Feb 25 '16 at 13:50
2

Using a random offset might be a faster alternative, than ordering by RAND() in the generated SQL query:

if( $count = wp_count_posts( 'advertising' )->publish )
{
    $random_ad = get_posts( 
        [
            'post_status'       => 'publish',
            'offset'            => rand( 0, $count - 1 ),
            'posts_per_page'    => 1,
            'post_type'         => 'advertising'
        ] 
    );
}

where we use the offset to set the LIMIT x, 1 SQL part, where x depends on the number of posts.

Note that this is might not be so great for very large number of rows, but there's also an interesting benchmark sheet by Josh Hartman.

Pieter Goosen has also an interesting approach in his answer, that should give you much better alternative than ordering by RAND().

  • Thank you for the answer as well! This too makes perfect sense. My only question about anything in this entire thread is what Pieters $q was for. I understand everything else for the most part in every answer. – Alexander Graham Feb 24 '16 at 21:28
  • Regarding the $q variable, I guess you mean the array of post ids, you could rename if you prefer, eg $post_ids ? That "q" could also be a shorthand for "query" @AlexanderGraham – birgire Feb 24 '16 at 21:32
  • Correct. I believe it's a catch-all if I read it right. – Alexander Graham Feb 24 '16 at 21:34
  • Strangely, your code is the slowest of our all on initial testing. Seems like wp_count_posts() adds a few extra dollars to the price ;-) – Pieter Goosen Feb 25 '16 at 4:25
  • 1
    it would be handy to have such *.sql available for download for others to play with. Maybe wxr could work for the small sizes and maybe one could generate larger one with e.g. wp-cli. But 1M or 10M might be more difficult to get/generate. Such a WordPress dev db-service should of course have various setups, for various sizes of taxonomies, meta, options etc. @PieterGoosen – birgire Feb 25 '16 at 9:56
2

First get the random advertising post by get_posts

$random_ad = get_posts(array(
    'numberposts' => 1,
    'post_type' => 'advertising',
    'orderby' => 'rand'
));

Now get the custom fields from this random post

if (!empty($random_ad)) {
    $random_ad = array_shift($random_ad);
    $link = addhttp( get_field('advertisement_link', $random_ad->ID));
    $image = get_field('upload_advertisement', $random_ad->ID);
}

Build your ad HTML and insert where you want!

$ad_code = '<a href="'.$link.'" target="_blank"><img src="'.$image.'" /></a>';
  • Of note, get_posts uses WP_Query internally, also this code doesn't check if $random_ad is empty correctly, and should use if ( !empty( $random_ad ) ) { instead – Tom J Nowell Feb 24 '16 at 16:29
  • Yes of course but good for getting single post and without setting up post data! – Sumit Feb 24 '16 at 16:33
  • 1
    Code updated... – Sumit Feb 24 '16 at 16:35
  • Ok, this is good, as I now understand a bulk aspect of what I need to do. However, I'm running into an error, Catchable fatal error: Object of class WP_Post could not be converted to string. Here's my pastebin. – Alexander Graham Feb 24 '16 at 16:42
  • 1
    @Sumit solved! Changed order to orderby. :) Thank you so much for the help! – Alexander Graham Feb 24 '16 at 18:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.