Conversation
Codecov Report
@@ Coverage Diff @@
## 2.x #2073 +/- ##
=========================================
Coverage 96.04% 96.04%
Complexity 1530 1530
=========================================
Files 50 50
Lines 3892 3892
=========================================
Hits 3738 3738
Misses 154 154
Continue to review full report at Codecov.
|
|
@gchtr and @pascalknecht , thanks so much for putting this together! This is really helping me focus on shipping some good code. Let me first just say that I like what I'm seeing. Keeping the API simple and small is definitely the way to go here, and I like that a Class Map can be either an array or a function. I think that'll keep things really simple for most use cases, yet endlessly extensible for more advanced usage. Just a few clarifying points before I dive in... I think we should make the
I don't think it's possible to detect who's calling a constructor (or any other method) without getting rrrreally hacky. :) But, we can guarantee it will never be called directly by making class Thing {
public static function build() {
return new static();
}
protected __construct() {
$this->stuff = 'asdf';
}
public function get_stuff() {
return $this->stuff;
}
}
Thing::build()->get_stuff(); // -> "asdf"This is a pretty common approach when implementing the Factory/Builder patterns in other OO languages, and I think it makes sense here. However, it does force our hand in terms of the 2.0 vs. 2.x rollout, since it would mean a breaking change instead of a deprecation we can make down the line. That's unfortunate from a timing standpoint because it means we have to do more work up front, but I think it's worth doing that anyway. This will force us to be consistent across the board at 2.0, and I don't really see an alternative.
What do you mean by class conflicts? Are you talking about composition over inheritance? Are you worried about problems with inheritance in core Timber code, in user code, or both?
I think renaming and deprecating the old Twig functions is probably a good idea. Seeing "Use
I think we should keep the API as close as possible, and not rename the method. I'm definitely not dogmatic about backwards-compatibility, but in this case we're talking about one of the single most important methods in the entire library. At the very least, we should keep the name the same. From what I can tell, the idea for the Assuming that's the case, I'm not a huge fan of changing the structure of this first argument, because it means users will have to update every single call to Here's the API I propose instead: public function get_posts( array $query, array $options = [] );This would allow us to extend the API in the future without changing the signature, and support whatever I'm sure I'll have more comments and questions as I move along but I feel like this is off to a good start. Thanks again. |
|
Thanks for your deep dive into the proposal!
I agree, we could improve that in the documentation 👍. I’m adding this to the list.
I see, that’s the way to go. Making the constructor protected would work. And yes, we would have to do that in 2.0, because it would be a breaking change.
Yes, exactly, I mean composition over inheritance. But probably not in Timber core, mostly in user code, when class maps are used to define which class is used when. For now, I don’t see any direct problem, but maybe there are warning signs that I don’t know of? You certainly have a better grasp of these patterns ;).
Well, of course! I totally forgot again about the simplest reason why it should be
Yes, you got that right!
Totally! Here it wouldn’t make much sense to make developers change every call to |
|
@gchtr sounds good! Yes, if you could update the docs that'd be great. I'll continue laying the groundwork for using Factories under the hood, everywhere. There a lot of direct instantiations in the tests...
Since we're talking about "userland" code, I don't think there's a whole lot we can do in core to prevent the kinds of problems that composition-over-inheritance addresses. I say this because these kind of problems are rooted in poor software design. Aside from the premise that custom posts may be instances of different classes, Timber is pretty unopinionated about architecture, and I think that's a good thing. I do think it would be a good idea at some point to have a guide recommending using traits to accomplish composition over inheritance in tandem with class maps, since traits seem to be a somewhat underappreciated feature of PHP, especially in the WP world, in my experience. I'll write up a future issue for that. |
|
@acobster Great, I’ll be updating the docs in this pull request, then.
Alright! I’d say we leave it at "some point". I think there’s no need to add documentation for problems that might arise at some point in the future. I’m never against an additional guide that shows some advanced usage pattern, though 😉. |
|
@gchtr I made a bunch of updates to the description per our discussion, and created some new issues that are big enough to be scoped and tracked separately. Please review. I'll continue working on the internal Factory API for now, and circle back to tying it in with the larger API changes. Thanks! |
|
@pascalknecht after the discussion on this ticket, I think that our best route here is, unfortunately, to close #1219 and #1793 and start on Factories fresh, with the new API proposal as the motivating goal. There are simply more moving pieces, many of them coupled to the old API (or to a new API that we're not going to use), than I can wrap my head around right now. I could try to refactor #1219 to match the new requirements, but I think that would take me about twice as long as just starting fresh and pulling in what I need to accomplish the end goal of this API. Unless you have the bandwidth to take on this major refactor of a completely new API (which also currently lacks tests), I'm going to go ahead and close out these two PRs. Thanks. |
|
@acobster @gchtr thank you for the thoughtful rundown of the issues and outcomes. Between teh limitations of PHP, WordPress and naming conventions, there's unlikely to be a perfect solution, but this one seems damn good (at the very least). Now that this seems like the appropriate path, closing #1219 and #1793 is probably the right move. Even if the exact code from @pascalknecht and @joshlevinson doesn't make it in — each of these PRs developed some of the key ideas we're now implementing. Happy to continue contributing my two cents — but mostly I'll stay out of the way as you two keep hammering!!!! |
|
@jarednova @gchtr another handful of questions for the team:
|
docs/guides/class-maps.md
Outdated
| 'book' => function( \WP_Post $post ) { | ||
| if ( 0 !== $post->post_parent ) { | ||
| return BookChildComment::class; | ||
| } | ||
|
|
||
| return BookComment::class; | ||
| }, |
There was a problem hiding this comment.
@gchtr I think this callback should still get the WP_Comment object, rather than the associated post. Otherwise, if someone wants logic based on comment metadata or something, there's no way to do it because the comment ID is no longer in scope. If folks want logic based on the associated post, on the other hand, they can always just call get_post($comment->comment_post_ID).
There was a problem hiding this comment.
@acobster Thanks, that’s a very good input! I updated the guide in 4012fd6.
When calling the filter, do you think we already have the associated post and could pass it as a second parameter, or do we only have the post type of the associated post?
Would (and should) this be possible?
'book' => function( \WP_Comment $comment, \WP_Post $post ) {
}Or even:
'book' => function( \WP_Comment $comment, \Timber\Post $post ) {
}|
Here's my take ... Should Timber::get_posts() always return a PostCollection? Will we ever want to return the more specialized PostQuery? Is there a distinction between the two that users will find useful going forward? The same method also sometimes returns a QueryIterator. Can this behavior be subsumed into PostCollection or PostQuery somehow? In other words, can we just always return some kind of PostCollection, and have the Factory layer make sure that that collection is initialized properly? It strikes me now that this is really a |
|
@jarednova @gchtr alright...buckle up. 🚀 Edit: updated names and added some clarifications.
I agree...insofar as we want a unified interface that we return every time.
By "this piece of functionality" do you mean translating from a
After some thinking, I believe that the best approach here is to define a interface PostCollectionInterface extends Traversable, Countable, ArrayAccess {
public function pagination( array $options = [] );
}
class PostQueryIterator implements PostCollectionInterface {
public function pagination( array $option = [] ) {
return new Pagination($options, $this->_query);
}
/* all the same traversal/counting methods currently in QueryInterface, plus ArrayAccess methods... */
}
class PostArrayObject extends ArrayObject implements PostCollectionInterface {
public function pagination( array $options = [] ) {
return null;
}
}A couple things to note:
We should document The long versionI hemmed and hawed over this issue for a while before coming to the above conclusion. In case anyone's interested, here's how I got there... In its constructor, to get the array of posts that it will internally iterate over, if ( is_array($args['query']) && count($args['query']) && isset($args['query'][0]) && is_object($args['query'][0]) ) {
// We have an array of post objects that already have data
$this->queryIterator = new PostCollection($query, $args['post_class']);
} else {
// We have a query (of sorts) to work with
$this->queryIterator = new QueryIterator($query, $args['post_class']);
}
$posts = $this->queryIterator->get_posts();
parent::__construct( $posts, $args['post_class'] );...meaning that This complicates the claim that:
Where this gets really sticky is that public function pagination( $prefs = array() ) {
if ( !$this->pagination && is_a($this->queryIterator, 'Timber\QueryIterator') ) {
$this->pagination = $this->queryIterator->get_pagination($prefs, $this->get_query());
}
return $this->pagination;
}I don't know if there's some subtlety elsewhere in the code guaranteeing that we never actually are instantiating All that is to say: it seems like what we're doing here abstracting away two different data sources (a primitive array of posts or a |
|
My questions/points all feel answered. To the point of:
Yes. I don't think we'll have anyone angry/care about whether it's a The proposal feels very reasonable and a sensible organization of the classes/interfaces we already have. Interested in @gchtr's interpretation on the above details as it relates to the factories component. |
Cool, thanks @acobster 👍!
From answering a lot of questions here in the issues and on Stack Overflow, I’ve found that in the current state, it’s not very clear what the difference between a As far as I understand it, I think your proposal makes sense. I suppose for developers it’s important they can:
So I agree with Jared when he says:
Yes, I think that would help developers understand what’s going on 👍. Remember to ping me if I can help here!
Yes, let’s wait until we actually need it, because it’s probably an edge case. The only case I ever needed to overwrite a class inside the post collection logic was a custom PostIterator, but even that won’t be needed with 2.0. |
|
Awesome. Thanks for the responses, @gchtr and @jarednova. I'll follow up with more specifics later in the week (client projects are calling me back) but I think we're in really good shape for the last big push for this API change. |
This reverts commit 679b01d.
Remove duplicate routing notes, fix inconsistencies between deprecated and deleted functions
Fix typo
|
As soon as #2367 is merged, I want to merge this pull request, too. We discussed and documented the new API thoroughly. The outstanding todos all have separate issues and we don’t have to wait for these to be resolved before we merge this. |
|
@gchtr agreed! I just did a once-over of the 2.0 tag and it's looking like we're in really good shape. Let's just keep an eye out for anything else that needs that tag that doesn't already have it. Once this is merged, how about we make an actual cc @jarednova |
|
Oh my. Are we actually here????!!!!! Yes! Let's start the tagging and moving to |
2.x Add upgrade documentation for THE PURGE
|
@jarednova @acobster Yes, I think we’re getting there! For me, there’s still quite some work for the documentation, but that doesn’t stop us from releasing an Alpha version 👍🚀🔥. I’ll let you one of you review and merge this. |
|
🎉 LTGM! I'll also pull this into the docs style updates (timber/docs#23) so we can more closely review that work with these new additions/changes |
|
woohoooo!! 🎉 @jarednova so how do we get the 2.0 docs up to https://timber.github.io/docs/v2? Also, I have started on a draft release - please take a look and add your notes when you get a chance. cc @gchtr |
|
@acobster @jarednova I'll have a look at everything tomorrow and will update the live documentation. |
Tickets: #1219, #1793.
@pascalknecht and I thought about what would be the most consistent way to go forward with Object Factories and the changes that come to the API. Here’s our proposal. As discussed in #1219, we’re starting with the documentation first.
How to navigate this proposal
The API
We’ve found that the new API could evolve around Class Maps. Class Maps define what objects will be used for getting single objects as well as collections of objects. We already have them in Timber, but we don’t use them in all the places yet.
When we only have Class Maps, we can use them as a central hub to set what classes are used in Timber. A single source of truth, so to say. We could remove the existing possibilities that we currently have in certain functions to pass in the class that should be used, like for
Timber::get_posts()andTimber\PostQuery().@aj-dl proposed to always be explicit when instantiating new posts in #1219 (comment). We think that is a good idea in general, at least better than always getting something from a
Postclass, which then might not necessarily be aPost, but a child class ofPost.However, by being explicit, we could only be explicit with objects that can be instantiated directly. But for all other functionality like
$post->thumbnail()where the instantiation is hidden in a function, we would still have to rely on the Class Map. It still wouldn’t be clear when exactly the classes we defined in the Class Map would be used.I myself have recently found that when I had to define the class that should be used in several places, I ran into a couple of cases where I was confused why something didn’t work, until I realized that I didn’t pass the class to be used in a function parameter. If we’d have Class Maps only, we wouldn’t have to think about instantiating objects with the proper class all the time.
Because of @aj-dl’s comments, we explored the different options we had for the API and thought about the following possibilities:
PostFactory::get()andPostQueryFactory::get()– Like I commented in other issues before, I would find it great if we wouldn’t introduce the concept of factories into the API, but keep it hidden behind the scenes. Developers should be able to start with Timber without having to understand the concept of factories.Post::get()andPostQuery::get()– Here we would clearly state what we get, but there could be confusion about the naming. And we would probably have to rename theTimber\Postclass to something likeTimber\BasePostand makeget()the only method inPost.Timber::post()orTimber::posts()– Here it might not be clear enough that you get something. It could also be understood as posting something.Which leads us to the proposed API. We think we should use the functions we already have:
Timber::get_post()Timber::get_posts()Timber::get_term()Timber::get_terms()This is basically what @acobster already proposed in #1219 (comment). All of these functions could rely on Class Maps. The benefits of this API would be:
Timberclass, developers get a nice overview of the API when looking at the code or the online documentation.setup()when using a custom class, because the context also uses a Class Map.The drawbacks:
@vardefinitions.Do you see any other benefits or drawbacks? We’d be pleased to hear them.
Factories
For the factories implementation, I think we’re still on track in #1793 and can continue discussing and working out the details about the factories themselves there.
This pull request can be used for the changes to the documentation (without documentation in DocBlocks).
Proposed Changes
When we stay with the proposed API, we can target to release Timber 2.0 sooner, because the only thing that’s holding us back is how the public API will look like. There are changes that need to be made for 2.0 and then there are changes that we can make later, in 2.x.
Changes for 2.0
For 2.0, we could implement updates to the API for post objects only.
timber/post/classmapfilter. Factories reboot #2093DeprecateUpdate the API to prohibit passing in the class to use for instantation in all post functions and use the Post Class Map as the single source of truth. Update ::get_post() and ::get_posts() signature #2090DeprecateUpdate the API to prohibit passing in query strings toTimber::get_posts(), like here:Timber::get_posts( 'post_type=article' );Update ::get_post() and ::get_posts() signature #2090DeprecateUpdate the API to prohibit passing in the class to use for instantation in all functions and use the new Class Maps as the single source of truth. Update ::get_post() and ::get_posts() signature #2090Timber::get_menu(),Timber::get_user(), andTimber::get_comment()methods. Implement Timber::get_menu() and Timber::get_user() methods. #2089, 2.x Update documentation for menus #2197Timber::get_*calls. Eliminate all direct instantiations of Timber\Core objects outside of Factories #2094Throw a doing-it-wrong exception when aMakePost[Term,Menu,CommentorUser] orPostQueryobject is instantiated directly. (Is that even possible?)Post,Term,Comment, andUserconstructors protected. Make core constructors protected #2088 (MenuandMenuItemconstructors will be made protected in Implement new PagesMenu API #2292)to_array()to Collection classes so that we can use something likeTimber::get_posts( $query )->to_array().Timber\PostClassMapfilter and rename it totimber/post/classmap. Currently, it’s calledtimber/post/post_classin the 2.x branch. 2.x Deprecate Timber\PostClassMap filter and rename it to timber/post/classmap #2293timber/*/classmapfilter callback returns something other than aCoreInterface. (Probably should throw an exception rather than just letting itTypeError.) - also 2.x Deprecate Timber\PostClassMap filter and rename it to timber/post/classmap #2293PagesMenuAPI. Implement new PagesMenu API #2292::get_image()and::get_attachment()top-level methods and corresponding Twig functions. Implement top-level image methods #2333Backwards compatibility
With the proposed API, we would replace the existing API for
Timber::get_post()andTimber::get_posts(). We will have to consider what happens with the differences betweenget_posts()andWP_Queryas discussed in #1957.Open Questions/Discussion
@jarednova, @pascalknecht, @acobster, @palmiak, @aj-adl: I’d be happy to get your opinions on this pull request. Some more questions came up while working on this pull request:
I know Inheritance is not always the best option for Object Oriented Programming. Does anybody see any problems with class conflicts?Are callback functions in the Class Maps enough to have advanced control over what class is used when?Post,PostQuery, etc. For consistency, should we consider renaming these functions toget_post(),get_posts(), etc?Should we useTimber::get_posts()orTimber::query_posts()? Would it be more clear thatquery_posts()returns a query/collection of posts, and not an array of posts?Todo
no_found_rows, see 2.x Documentation for API #2073 (comment). → Documented in https://github.com/timber/timber/blob/2.x-docs-api/docs/v2/guides/posts.md#count-rows-only-if-neededTimber::get_posts( $query )->to_array(), see 2.x Documentation for API #2073 (comment).timber/get_posts/mirror_wp_get_postsfilter. Done in 2.x Add upgrade documentation for THE PURGE #2367.