2.x Refactor context and third-party compatibility#1778
Conversation
|
Hi @gchtr thanks for this PR, it's on my list for today. For now are you just looking for feedback on those discussion items or should I start a more thorough review for merge? |
|
@jarednova I guess first it would be enough to hear what you think about
(As usual 🙃) it’s quite a long text, but I like to list my thoughts and how I came to an idea. I haven’t tested it, so that would be the next step. And then write tests for it. And then I guess you could do a more thorough review. Except if you want to take over from here 😉. |
|
One random opinion: The suggestion of ways to modify the PostQuery seem a bit strange to me. Why not something like this: $posts = new Timber\PostQuery( [
'query' => [
post_parent' => 0,
],
'reconcile_query' => 'merge', // could also be a boolean flag to replace or merge query, the name of this is less important
] );This allows PostQuery to have a single array of options which feels a little cleaner and easier to handle. EDIT: Also I have read this issue a few times but still don't feel like I fully understand it, so feel free to ignore me… but that said, I also feel like the static |
|
@gchtr Here are my comments about your PR: 1. Global contextI think the post/s should be in the global context as in WordPress they are also in global scope for each page and there is no neeed to get them explicitly. At the moment I just think the documentation was too unclear about that. 2. Prepare template for the current postYes we really need to have this as already mentioned in the discussion at mindkomm/timber-integration-woocommerce#6 I also think that if we have a setup function we will also need a teardown function. Currently the What I really like is the 3. Change the default queryWhat I don't really get is the In general great ideas. These features/improvements will be great to have. |
Sorry, if this is a little confusing. But there’s a lot going on. I already thought a lot about this. It started out with #1639, which led me to #1766 and then on to this pull request. I didn’t explain everything in this pull request. Probably it’s required to read the linked issue and pull request as well 🙈. Additionally, #1771 is a side issue that plays into this, too. That’s why your comments are truly appreciated! We need to be on the same page for this. I can’t decide alone how Timber should work. I know, my comments are quite long at times, but I like to list the options that we could choose and think about the pros and cons. To explain a bit more, I’m gonna start with point 2 and come back to point 1 about the global context in the end. 2. Prepare template for current postA teardown function
True! I thought about a reset/teardown function after queries in #1771, but not for having it on a post itself. Great point! I think a teardown function would be great to reset all the data that was set up in the setup function. Automatic setup
Having a call on post instantiation is the thing I tried to avoid after gathering all weird issues about this in #1639. Consider the following example: $context = Timber::context();
$context['post'] = new Timber\Post();
$context['myotherpost'] = new Timber\Post( 744 );
Timber::render( 'single.twig', $context );In this example, when As long as you’re not in the template to display something, I think there’s no use in setting up a post and calling hooks, because every new instantiation would just overwrite the one before it. This includes the functions that can be used in Twig: <img src="{{ Image(my_global_logo_id).src }}">This would set up the "context" for the content to follow. I myself try to avoid this already. But it’s possible in Timber and developers use it, so we need to consider this as well. I guess a teardown function wouldn’t change the problem. A teardown function is great for loops over Timber posts, but doesn’t help in the context of a singular templates. Tell me if I’m missing something! Call setup for singular templatesFor the reasons listed above I still think that for singular templates, we have to tell Timber manually which post it should use as the "main post". I’m again listing the options I came up with: a) Solve it through contextThis is practically the functionality I initially had in mind in #1766. The guide for how this solution would be used can be found here. The idea is that you pass the post you want to setup to the context. By default, it would take $context = Timber::context();
var_dump( $context['post'] ); // Outputs the $post globalIf you’d want to set a different post, you could use: $context = Timber::context( array(
'post' => new Timber\Post(),
) );Internally, the // Use the $post global.
$context['post'] = Timber::context_post();
// Use a custom post.
$context['post'] = Timber::context_post( new Timber\Post( 85 ) );Maybe we would need to consider different names instead of b) Use the
|
|
@gchtr I wouldn't remote the PostsIterator as it makes sense to have it. The fact that you included loop code in the setup method says that one such class would still be good to have to not make Timber\Post responsible for too many things. class PostsIterator extends \ArrayIterator {
/**
* Calls the setup function of the current post
* to setup post data
*
* @return mixed
*/
public function current() {
// Fire action when the loop has just started.
if ( 0 === $this->key() ) {
do_action_ref_array( 'loop_start', array( &$GLOBALS['wp_query'] ) );
}
$post = parent::current();
$post->setup();
return $post;
}
public function next()
{
$post = $this->current();
$post->teardown();
// Fire action when the loop has ended.
// Maybe there is a better solution for this if
if ( $this->count() === $this->key() ) {
do_action_ref_array( 'loop_end', array( &$GLOBALS['wp_query'] ) );
}
parent::next();
}
} |
|
@gchtr I think the "Here’s what I prefer now" section of your last post there seems like a good compromise! The one thing I would reiterate from my original comment (and this is not specifically referencing any of your examples or anything, just a general observation) is to be careful of too much magic. I think explicitness is always better, even if it means a extra lines of code, extra characters, etc. I love Timber but think it sometimes errs on the side of being a little too clever and magical. Yes, explicitness requires developers to do a little more work, but I think it promotes a better understanding of how Timber interacts with WordPress and better development patterns. I called out static methods in particular, but I think it's worth thinking about in all situations. That's one reason I like the explicitness of setting a 'query' item inside a single Really excited to see this come together and thanks for tolerating my philosophical rambling :) |
|
I wouldn’t remove PostsIterator either. I think putting the And it looks like @pascalknecht found the solution for #1771 🎉. Thanks! We can also put @chrisvanpatten Well, it looks like we’re on the same page here!
Yes! Totally agree! I’m probably guilty of trying to add some sprinkles of magic here and there. You make a really solid argument for having only one array for |
This is actually a good example of what I think is bad magic, but I'm also the type of person who gets frustrated that WordPress does this ;-) Generally my take is that it's better to ask developers to be specific about the type of data they want passed into the Twig template. It keeps the core pretty minimal and allows you to more easily use your own objects extending from core classes (or replace them entirely). It also means less of an ongoing burden to maintain compatibility for the various types of data inside context… because once something goes Into context, it will be really hard to pull out without breaking a lot of things. |
|
@gchtr I'm just catching up on all the back-and-forth I missed. I think you, me, @chrisvanpatten and @pascalknecht all are on the same page philosophy-wise. Let me know when you're ready for me to take a full in-depth review of the code/tests |
|
@gchtr what should we do with |
True that! Extensibility is a good point as well. Though for Timber, I guess there are also a lot of developers who use it without much of their own OOP coding. A cool thing about Timber is that it has a low entry level, but allows to bring in your own complexity.
Definitely! The thing here is that currently
@pascalknecht Good catch! No, we wouldn’t need the I pulled in Pascal’s pull request.
@jarednova I guess we’re still trying to find the best conceptual solution before we dig deep on tests. But if you have any input on that, it would help as well! I think the biggest question we should solve first is: How do we setup a post for a singular template? How and when do we call a post’s |
ChangesHere are the changes since my last comment:
Tests
ExplanationHere’s the current behavior:
Automatic template contextsI thought again about not adding too much magic. I kinda liked the idea to not introduce parameters for For singular templates, using a custom class would mean that you have to call the // Using an extended class
$context = Timber::context();
$post = new Extended_Post();
$post->setup();
$context['post'] = $post;
// Or very short
$context['post'] = ( new Extended_Post() )->setup();For archive templates, they can write a new // Using a custom query
$context = Timber::context();
$context['posts'] = new Timber\PostQuery( array(
'query' => array(
'posts_per_page' => 3,
)
) );
// Merge with default query
$context = Timber::context();
$context['posts'] = new Timber\PostQuery( array(
'query' => array(
'posts_per_page' => 3,
),
'merge_default' => true,
) );Here I added the argument style for A question I had was: How can I change the post class that Timber will use when I can’t pass in arguments to the context? // Using a custom post class
$context = Timber::context();
$context['posts'] = new Timber\PostQuery( array(
'query' => array(
'posts_per_page' => 3,
),
'post_class' => 'Book',
) );The recommended way would probably still be use the PerformanceIn any case where you run a new As long as you don’t change any query parameters, no additional database query will be run. Considerations
TODO
Apart from the documentation, I’d say this pull request is "ready". It’s the best I have now. Suggestions are still very welcome. |
|
@gchtr Just had a look at the code. It looks really good and clean. |
|
@pascalknecht We could easily put that into |
Removed almost empty methods
|
I updated the context guide to reflect the current implementation. I removed the todo for documenting From my side, this pull request is finished. At least for the current state in 2.x. Depending on what happens with object factories we maybe have to update the context again. |
|
Awesome! I'll give things a final pass and merge |
|
@jarednova Thanks for taking a detailed look at the context guide as well as the rest of the code. What do you think, is this ready to merge now? |
|
'tis! Let's make it happen |
I thought about the context again and I think I don’t like the solution I proposed in #1766 🙃. I think it might be easier if template contexts didn’t work dynamically. There are a couple of reasons:
context(), and some afterwards. I just feel like this leads to templates that are hard to read.postorpostsin their context, but more explicit names. For an event post type, usingeventsinstead ofpostsmight be more preferrable. The workaround to get that would be too complicated.postswas already set in the context. I think people won’t rewrite all their templates for some little magic. We’d only harm performance if we’d leave it here.I guess I wanted add too much magic. I think we should remove the magic from the context and make it easier to expect what you get. Back to the drawing board!
What I initially wanted to achieve was:
postsare set.Let’s have a look at these goals in more detail:
1. Global context
I think the global context works fine when we remove
posts. Going on,$context['posts']needs to be set manually. Having to write a little line of code is probably no harm, but will make the code more readable in terms of «What do I have in my context?».2. Prepare template for the current post
What we need to do is basically have a
the_post()function for Timber. This function is used in WordPress to setup the "context" for the post that is about to be rendered. We already have this for post loops in Timber, but not yet for singular templates that don’t have any loops.For having some kind of the_loop for singular templates, I see different approaches:
A context_post() function
I first thought that
context_post()might be a good idea.We can now see that we have
postin the context. By not passing anything tocontext_post(), we could assume that it uses some defaults. The currently displayed post maybe?Use it with a custom post
Use your own class
A setup function
The new proposed method for fetching a Timber post in Timber 2.x is not
Timber::get_post(), butnew Timber\Post().So I’d rather leave it like that and not have a wrapper function like
context_post()that takes care of the setup. So we’d need a separate setup function. Now, where could we place a setup function? (The following thoughts were sparked by the discussion in mindkomm/timber-integration-woocommerce#6)A global
the_postfunctionThere could be a function like the following that does the setup for us. By calling it
the_post(), we stick to what people might be used from using WordPress.Now the question is that e.g. for setting up a post for WooCommerce, we need also need to setup a
$productglobal. Luckily, WooCommerce uses thethe_postaction, so that would be covered here. But is there a more elegant solution?A
setupmethod onTimber\Postor even
A setup method would probably be more flexible. I think that maybe the right place to call a setup method is not globally, but on a post itself. By having it in a separate
setup()method and not in the constructor, we can decide when we want to call it.This is what the
PostsIteratorcurrently looks like:Instead of having a posts iterator care about what to do with the post, we could call the setup method instead:
And then, the setup method would look like this:
And for a WooCommerce Product class that needs to call
wc_setup_product_data()in addition to whatPost::setup()already does, it could look like this:This would even be better for the WooCommerce integration, because we wouldn’t need a separate
ProductIteratorclass. We could do it in thesetupmethod.Even if somebody needed to add more logic, they could write their own class that extends a
Productclass and overwrite thesetup()method. Straightforward.The only question that would remain: after looping, how do we set back the query (discussed in #1771)?
3. Change the default query
Archive templates don’t seem to be much of a problem to set up, because all of the heavy work is done when looping over posts. The missing piece I actually wanted to add was to change the default query that is used.
The new proposed method for fetching a collection of Timber posts in Timber 2.x is not
Timber::get_posts(), butnew Timber\PostQuery(). By not passing in any variables, it would fetch the default query.I first thought that a
Timber::context_posts()function might be a good idea.Changing the default query would mean to pass new arguments to it. The passed arguments would be merged with the default query.
But calling it
context_post()now seems to be a bit unclear to me. Maybemerge_default()would be better. But should it then be added to theTimberclass?Maybe we should be able to tell
PostQuerywhat it should do with the parameters that we pass in. By using amerge_defaultargument, we could tell it to merge the passed arguments with the default query.Coming back to a wrapper method: Why not add it as a static method to
Timber\PostQuery()?Solution
This pull requests:
context_post()andcontext_posts()functions.setup()function toTimber\Postand updatesTimber\PostIteratorto work with it.merge_default()function toTimber\PostQuery.Discussion
I guess sometimes we need to take a detour to come to a better solution. Are there any thoughts? Things I missed? Is it a direction that will work in the future? I’d love to hear feedback.
I didn’t write any tests yet, because I first wanted to put my thoughts out there.
TODO
QueryIterator?