Skip to content

2.x Refactor context and improve compatibility with third party plugins#1766

Merged
jarednova merged 16 commits into2.xfrom
2.x-context
Aug 27, 2018
Merged

2.x Refactor context and improve compatibility with third party plugins#1766
jarednova merged 16 commits into2.xfrom
2.x-context

Conversation

@gchtr
Copy link
Copy Markdown
Member

@gchtr gchtr commented Aug 15, 2018

Ticket: #1639

This pull request refactors Timber’s context and improves compatibility with third party plugins.

Improving compatibility for third party plugins

As lined out in #1639, Timber’s current behaviour can lead to compatibility problems with plugins such as BuddyPress or WooCommerce, because…

  • the_post hook is called whenever a Timber\Post object is instantiated.
  • Loop variables are not set for singular templates.

I figured that a way to fix both problems would be to use Timber’s context function, because that’s the function we always call in a template. Using a WordPress hook like template_redirect could be tricky, because developers probably will set different posts as the main post for singular templates.

That’s why I added the necessary logic to mimick WordPress’s behavior for singular templates:

  • Sets $wp->query->in_the_loop = true.
  • Calls the loop_start hook.
  • Calls $wp_query->setup_postdata().

And I removed the call to the_post inside Timber\Post::get_info() and moved it to a context function.

A context guide

I added a new Context Guide in docs/guides/context.md that will explain the concept of the context in Timber. It’s probably best to read through it in addition to what is written here.

Rename function to context()

To be consistent with the naming that we applied in the rest of Timber’s classes, I renamed the get_context() function to context(). The get_context() function will be marked as deprecated.

New functions

Next to Timber::context(), I added three functions to better handle the different context caches.

  • Timber::context_global() – Used for getting the global context.
  • Timber::context_post() - Used for getting the context for a singular post.
  • Timber::context_posts() – Used for getting the context a post collection for an archive.

Template based contexts

The context changes based on which template is displayed. For singular templates, it will set post in the context, and for archive templates it will set posts in the context.

This way, Timber will make assumptions about what you’re gonna do in your template. Instead of setting up your template like this:

archive.php

<?php
$context          = Timber::get_context();
$context['posts'] = new Timber\PostQuery();

Timber::render( 'archive.twig', $context );

You could now set it up like this:

archive.php

<?php
// Minimal style
Timber::render( 'archive.twig', Timber::get_context() );

(Little known fact: You wouldn’t have to use $context['posts'] = new Timber\PostQuery(); in the current version of Timber either. Timber did always set posts in the context.)

Of course there are a lot of cases where we need to adapt the default. That’s where context arguments are coming in.

Arguments for the context

The context now accepts an arguments array. Currently, there are three arguments supported: post, posts and cancel_default_query.

posts

The posts argument can be an array of posts, a Timber\PostQuery or an array of arguments that will be passed to Timber\PostQuery. If it’s set to false, Timber will not set posts in the context.

archive.php

$context = Timber::context( array(
    'posts' => array(
        'post_parent' => 0,
    ),
) );

Timber::render( 'archive.twig', $context );

Up until now, there was no easy way to adapt the default WordPress template query. Now, if posts is an array of arguments, it will merge this array with the default arguments that WordPress uses for its template query. So, in the example above, you will get the default list of posts for an archive, just without child posts.

It’s also possible to pass a Timber\PostQuery object to posts. Then, no merging will happen.

cancel_default_query

By default, parameters passed with posts will merge with the default WordPress post query. If the cancel_default_query parameter is set to true, merging will be disabled. Instead, the default query will be overwritten with the parameters passed in posts.

post

This can be a post ID, a WP_Post object, a Timber\Post object or a class instance that inherits from Timber\Post.

single.php

<?php

// Use a custom class for post.
Timber::render( 'single.twig', Timber::context(
    'post' => new Extended_Post(),
) );

You now might be tempted to try something like this:

<?php
// Don’t do this!

$context         = Timber::context( array( 'post' => false ) );
$context['post'] = new Extended_Post();

Timber::render( 'single.twig', $context );

While this might seem fine, it’s probably not a good idea. Setting post after running Timber::context() will not set up the necessary globals for compatibility with third party plugins.

As outlined in #1639, we can’t set up the globals inside Timber\Post, because that leads to other problems. It’s important to tell Timber for which post it should set up globals, in case a different post ID is used.

But how do we ensure that developers won’t use the example above, because it has become so prevalent in most Timber examples? And what if figuring out the post to display depends on what the context returns?

The only solution I can come up with now could be to use Timber::context_post() separately:

<?php
$context         = Timber::context( array( 'post' => false ) );
$context['post'] = Timber::context_post( array( 'post' => new Extended_Post() ) );

Timber::render( 'single.twig', $context );

Caching

Timber::context() can be called more than once in a page load, for example inside hooks that display only a small component of a page. That’s why it has a cache. But because the context can now be dynamic, I see two ways to go:

Option 1 – Only cache global context

If we only cache the global context and don’t cache template contexts, we’d probably have to advise developers to always set the post and posts parameters to false when they use Timber::context() inside actions, because otherwise additional queries would run.

Or we could make it a standard to only use the global context via context_global():

add_shortcode( 'global_address', function() {
    return Timber::compile( 'global_address.twig', Timber::context_global() );
} );

Option 2 – Cache everything separately

If we introduce separate caches for global variables and template contexts, there would be four caches:

  • $context_cache: A cache for all global context variables (site, request, theme, user, http_host, wp_title, body_class) as well as context data set through the timber_context filter.
  • $context_chache_post: A cache for post data in singular contexts.
  • $context_chache_posts: A cache for post collection data in archive contexts.
  • $context_args: A cache for the arguments passed to context.

The result for Timber::context() will be put together from these caches.

It should still be possible to run Timber::context() with different arguments after a cache is already set. To be able to ignore the post and posts cache by passing in new arguments, there’s the $context_args cache. This cache will save the arguments from a call to Timber::context(). Whenever arguments are passed to Timber::context(), it will compare the arguments with the arguments saved in this cache and will either return the cache or the result for the new arguments, ignoring the cache. If a new query runs based on different arguments, this query won’t be saved in the cache. The cache from the first run will persist, including the query saved in there.

Does this logic make sense? How should the context cache behave?

Personal opinion

I’d settle for Option 1, because I think it might be easier to describe and understand. In my 3+ years developing themes with Timber, I only ever required the global context inside hooks.

New filters

I first thought about using a lot of filters to change the context defaults, but then I realized it’s not really necessary. One new filter would suffice:

timber/context/args

With this filter, we can change the arguments that are passed to Timber::context(). When added globally in functions.php, it will affect all queries. But it could also be added in a template file like single.php or archive.php to affect only certain templates. Here’s an example:

functions.php

// Globally disable `post` and `posts` in context
add_filter( 'timber/context/args', function( $args ) {
    $args['post'] = false;
    $args['posts'] = false;

    return $args;
} );

You could also use some logic here:

// Globally disable `post` and `posts` in context
add_filter( 'timber/context/args', function( $args ) {
    if ( is_post_type_archive( 'book' ) ) {
        $args['posts'] = false;
    }

    return $args;
} );

Discussion

The changes introduced here might be opinionated, but I thought it’s probably easiest if I propose a solution that we can then discuss.

  • Should we call it cancel_default_query, run_default_query, overwrite_default_query or merge_default_query?
  • Should cancel_default_query (or any other name we choose) also be used to tell Timber to not set post in the context?
  • Does the caching logic introduced for the context make sense? How should the context cache behave? Should we only cache the global context?

Testing

I added tests, but testing is a little hard, because Unit Testing doesn’t work well with Conditional Tags, because it’s not really meant to be used for this.

Todo

  • Complete the Context Guide when discussion is finished.
  • Update documentation where Timber::get_context() is used instead of Timber::context().
  • Improve inline documentation for parameters, filters, etc.
  • Add changes to upgrade guide.

@gchtr gchtr added the 2.0 label Aug 15, 2018
@gchtr gchtr self-assigned this Aug 15, 2018
@gchtr
Copy link
Copy Markdown
Member Author

gchtr commented Aug 21, 2018

@jarednova Could I get your overall opinion and your opinion on the points in the Discussion section before completing this pull request?

@jarednova
Copy link
Copy Markdown
Member

Sure thing! will wade into today

@jarednova
Copy link
Copy Markdown
Member

jarednova commented Aug 22, 2018

@gchtr this is a great solution to (2) very old problems. My outstanding Qs are around the areas you highlight:

1. To cancel or not to cancel?

My thinking here is because 95% of the time people will just use the default query, and most of that other 5% will be about query modifying ("I want 20 posts per page instead of the default"). Thus, this is for the <1% of times. Given that cancel or overwrite are the most sensible verbs (since you're interrupting the default behavior. At first, I was leaning towards "overwrite" (since if you're sending an array to posts this is the literal effect of your action) but I can see how that could be confused with "overwrite the defaults with my specific values here." Thus, I land at cancel_default_query being the most clear (though perhaps imperfect)

2. Cache

I have to admit, I'm rusty on some much of the caching use/logic. I think I understand the theoretical advantage to option 2, but also think like it seems more complicated than the potential benefit. As you note, even as pro Timber developers (indeed, we make the darn thing) it's an edge case beyond what we'd even need. I'd prefer to err on the side of simpler and wait for the people to demand a more in-depth solution.

Overall this accomplishes ...

  • Make Timber play nicer with popular plugins
  • Make the API for context simpler/cleaner/morefun

I'd call that a huge 🎉 . Let's clean up the rest of the context vs. get_context stuff and the rest of the todo to pull this into 2.x — great work!

@gchtr
Copy link
Copy Markdown
Member Author

gchtr commented Aug 26, 2018

Okay @jarednova, thanks for having a look at this. I think it’s now ready to be merged.

  • The context now works with option 1, so Timber will only cache the global context.
  • I improved the Context Guide to describe in more detail how it works.
  • I changed the parameter for Timber::context_post(). It will not accept the same arguments array that can be passed to Timber::context(), but a post-like argument, which means a post ID, a WP_Post object or a Timber\Post object.
  • I updated all examples I could find where the context is used.

@jarednova
Copy link
Copy Markdown
Member

🎉
this is a great add — thanks for your work as always

@jarednova jarednova merged commit 8a39fd8 into 2.x Aug 27, 2018
@jarednova jarednova deleted the 2.x-context branch August 27, 2018 17:15
@gchtr gchtr mentioned this pull request Sep 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants