Timber: How to create Timber::get_posts inside a Twig template

Created on 9 Jul 2015  路  4Comments  路  Source: timber/timber

I have a custom post type I use to display banners on my site.
Besides the main banner information, I trying to display a loop of 3 related WooCommerce products, picked from a ACF relationship field.

I can just run {% for post in banner.get_field('products') %} and that does retrieve the data, but not as I would hoped for. It returns a regular posts object but in this case, being WooCommerce posts, I would rather use Timber's Timber::get_posts so that I can use the tease templates and have the hooks running with the right context.

How would you approach this, any ideias?

Update:
I though I would elaborate a little more on my use case in the hopes someone can give a hand with this.

Let's say im passing to my template a banner post type loop, for which the query is something like:

    $args = array(
        'numberposts' => 5,
        'post_type'   => 'banners',
        'tax_query' => array(
            'relation' => 'AND',
            array(
                'taxonomy' => 'banner_pos',
                'field'    => 'slug',
                'terms'    => array( 'home-main' ),
            )
        ),
        'order'       => 'ASC',
        'orderby'     => 'menu_order'
    );
    $context['banners_top']       = Timber::get_posts( $args );

Now inside my template, lets say home.twig, I call a new twig file which contains the logic that will act on the data passed to $context['banners_top']


<div class="banner-home {{ banners_pos }}">
    <div class="inner" am-slider="{{ slider_params }}">

            {% if banners_top %}

                {% for banner in banners_top %}
                <article class="entry" style="background-image: url({{ TimberImage( banner.meta( 'banner_image' ) ).src('banner_home_main') }});">

                    <div class="container">

                        {% if banner.manual_link %}
                            {% set link = banner.manual_link %}
                        {% elseif banner.banner_link %}
                            {% set link = fn( 'get_permalink', banner.banner_link|first ) %}
                        {% else %}
                            {% set link = undefined %}
                        {% endif %}

                        {% if banner.title or banner.description or banner.button_text %}
                        <div class="wrapper">
                            <h2>
                                {% if link %}
                                    <a href="{{ link }}" class="entry-title">{{banner.title}}</a>
                                {% else %}
                                    <span class="entry-title">{{banner.title}}</span>
                                {% endif %}
                            </h2>
                            <hr>
                            <div class="description">
                                {% if banner.description %}
                                    <p><span>{{ banner.description }}</span></p>
                                {% endif %}
                                {% if banner.button_text and link %}
                                    <hr>
                                    <a href="{{ link }}" class="button">{{ banner.button_text }}</a>
                                {% endif %}
                            </div>
                        </div> <!-- wrapper -->
                        {% endif %}

                        {% if banner.products %}
                            <div class="loop associated-products">
                                {% for post in banner.get_field('products') %}
                                    <h4>{{ post.post_title }}</h4>
                                {% endfor %}
                            </div>
                        {% endif %}

                        {% if link %} <a href="{{ link }}" class="trigger"></a> {% endif %}

                    </div> <!-- container -->

                </article>
                {% endfor %}

            {% else %}

                <article class="no-entries">
                    <div class="container">
                        <div class="wrapper">
                            {{ fn( '__', 'No Banners Published' ) }}
                        </div>
                    </div> <!-- container -->
                </article>

            {% endif %}

    </div> <!-- inner -->
</div> <!-- home-banner-top -->

The relevant part here is:

{% if banner.products %}
                            <div class="loop associated-products">
                                {% for post in banner.get_field('products') %}
                                    <h4>{{ post.post_title }}</h4>
                                {% endfor %}
                            </div>
                        {% endif %}

banner.get_field('products') is an ACF relationship field (WooCommerce products) for which I would like to loop inside my banner. I do get a regular post object and can loop through them. But I would like to have them available as a Timber object so that I can have a few utilities to have the posts available in the right context.

For example, with the code shown above I don't have access to things like prices, discounts, etc. This also happens with regular loops, unless you apply a quick fix to set the right context as @jarednova explained me before. Im guessing the same needs to happen here but im having trouble getting it to work.

Many thanks.

question

Most helpful comment

@lmartins I think the solution might be as simple as applying the TimberPost function to the result, so in the relevant part...

{% if banner.products %}
    <div class="loop associated-products">
        {% for post in TimberPost(banner.get_field('products')) %}
            <h4>{{ post.title }}</h4>
            <div class="price">{{ post.price }}</div>
        {% endfor %}
     </div>
{% endif %}

... this is the same as converting to a TimberImage, etc.

As a separate/similar note, I want to expose the global Timber object in the Twig template in the next version, so that you could do something like...

{% for product in Timber.get_posts('post_type=product') %}
    <h2>{{ product.title }}</h2>
{% endfor %}

All 4 comments

@lmartins I think the solution might be as simple as applying the TimberPost function to the result, so in the relevant part...

{% if banner.products %}
    <div class="loop associated-products">
        {% for post in TimberPost(banner.get_field('products')) %}
            <h4>{{ post.title }}</h4>
            <div class="price">{{ post.price }}</div>
        {% endfor %}
     </div>
{% endif %}

... this is the same as converting to a TimberImage, etc.

As a separate/similar note, I want to expose the global Timber object in the Twig template in the next version, so that you could do something like...

{% for product in Timber.get_posts('post_type=product') %}
    <h2>{{ product.title }}</h2>
{% endfor %}

Hi Jared,
Many thanks for your input here.

After my initial post I was actually doing something similar:

                        {% if banner.products %}
                            <section class="BannerHome-products">
                                {% if banner.box_title %}
                                    <header><h3 class="elDivider">{{ banner.box_title }}</h3></header>
                                {% endif %}
                                <div class="loop loop--products loop--minibanners">
                                    {% for entry in banner.get_field('products') %}
                                        {% set post = TimberPost(entry) %}
                                        {{ fn('timber_set_product', post)}}
                                        ...
                                    {% endfor %}
                                </div>
                            </section>
                        {% endif %}

Sadly thought, with either solution I get the following error:

<b>Fatal error</b>:  Call to a member function get_price_html() on null in <b>/var/www/html/wp-content/plugins/woocommerce/templates/loop/price.php</b> on line <b>17</b><br>

For some reason Timber is unable to set the right context for each product.
I'll keep digging this to see if I can find any solution. If you have any other ideas please do share.

You side note about exposing the global Timber object looks really useful. Can't stop preaching about Timber to everyone I know working on WordPress projects ;)

Ok, just to add to this.
Do you remember the little utility function you suggested to set the right context for product loops?

function timber_set_product( $post ) {
    global $product;
    var_dump($product);
    if ( is_woocommerce() || is_search() ) {
        $product = get_product($post->ID);
    }
}

The var_dump shows that for some reason if the context im trying to achieve here the global $product isn't accessible.

This is what I get from that var_dump. At the top is the loop im trying to build (it shows null for the var_dump not visible in the screenshot), bellow the object is printed with success for a normal loop:

screenshot 2015-07-15 09 39 37

This explains why the hooks specific for WooCommerce products cannot run. Only thing missing is understanding why the global $product is inaccessible in this case.

Thanks again Jared.

Ok, got it to work.
I don't really remember why but the function call wc_get_product need to be wrapped in a if condition. Just added the is_front_page which was obviously missing for this scenario.

function timber_set_product( $post ) {
    global $product;
    if ( is_woocommerce() || is_search() || is_front_page() ) {
        $product = wc_get_product($post->ID);
    }
}

Hope this do not induces any unforeseen issues.

Thanks again for your suggestion @jarednova, it ultimately pointed me in the right direction. :)

Was this page helpful?
0 / 5 - 0 ratings