WIP: Feature/interfaced object factories#1793
Conversation
This style provides different factories for Posts vs Terms, and allows direct instantiation of the factory vs strictly a static method. This is more future proof than the purely static method of #1218, and opens the door for stubs and better testing.
…edence Previously, the function argument for the class was only considered if the Class Map filter didn't yield a valid class This changes the logic so that if a class is specified, it takes priority The final is still filterable via `Timber\[Post|Category]ClassMap`
|
Current todo
|
|
Hey @pascalknecht I'd like to help out here if I can. There are lots of different tasks flying around so I wanted to consolidate the discussion and make sure everybody's on the same page before we move too fast on something as big as this. :) To that end, I'm reposting my comment from #1219 here:
What I propose is this:
That would let users do this: use Timber\Timber;
$post = Timber::get_post( $post_id );
$image = Timber::get_post( $image_id );
get_class( $post ); // -> "Timber\Post"
get_class( $image ); // -> "Timber\Image"Furthermore, say you've declared use Timber\Factory\TermFactory;
class MyTermFactory extends TermFactory {
public function get_class( $type, $taxonomy ) {
if ( $taxonomy === 'my_custom_taxonomy' ) {
return MyTaxonomyTerm::class;
}
// implicit else
return parent::get_class( $type, $taxonomy );
}
}
add_filter( 'timber/factory/term', function() {
return new MyTermFactory();
});
$tag = Timber::get_term( $some_tag_id );
$custom_term = Timber::get_term( $custom_term_id );
get_class( $tag ); // -> "Timber\Term"
get_class( $custom_term ); // -> "MyTaxonomyTerm"What's happening under the hood in my mind is that I believe this preserves backwards compatibility nicely while allowing complete extensibility around the factory system. The other thing I wanted to call out before we go too far down the road of testing this stuff is all these static methods. I've found that putting as much logic as possible inside instance methods and making static methods as few and as short as possible makes the code much DRYer, more reusable, and easier to reason about, and often much easier to test. It also allows the proposed Would love to hear your thoughts on this and see whether/how you might approach splitting up this work. Let me know! 🌲 |
|
@gchtr thanks for the heads up, moving discussion over here. I wanted to reply directly to some of your ideas in your comment:
Yeah, a fully fledged custom Factory class might be overkill sometimes, and this filter would serve users well for those cases. Implementing and injecting your own Factory class should probably short-circuit the, as having both a custom
See my comment above. I'm not married to keeping the |
I agree.
True that. I figured |
|
@acobster So let's get on the drawing board here 👍 I would prefer having seperate classes for the object factories as this is more clean. I kinda like the Example: // single.php or page.php
$context = Timber::get_context();
$context['post'] = Post::get(get_queried_object()->ID);The same thing would be needed for term pages // taxonomy.php
$context = Timber::get_context();
$context['term'] = Term::get(get_queried_object()->ID);When the id is undefined a null object or null should be returned. I don't like when it returns a fully fledged object if the post is not found. Then we will think about the implementation of the filters. I will need to think about this further. |
|
@pascalknecht sounds good.
Fine with me, as long as we're not locking calling code in to a specific factory class. :) That syntax looks nice to me.
Yes, relying on global state in library code scares me too. Let's get rid of it!
Could you say more about this? Not sure what you mean. |
My only fear was that writing $context['post'] = Post::get(get_queried_object()->ID);So pretty much the only time where you’d have to pass in the |
|
@gchtr Yes exactly. So we will have the best of both worlds: Stateless factories and easy implementation.
@acobster The factory should be able to map certain post types to a specific subclass of |
|
@gchtr @acobster @jarednova Do we all agree on this new API or do you have any inputs/objections/improvements? |
|
I agree! No objections so far. |
|
@pascalknecht ah ok, so specifically it's mapping |
|
@gchtr @acobster I created a first test implementation here: https://github.com/timber/timber/tree/new_object_factory . I was able to get rid of the PostClass String as we can now use polymorphism for this. Not yet sure whether we should use a seperate class for the Factory. We will probably also be able to get rid of all hardcoded class names with the factories, |
|
@pascalknecht what should we do about querying multiple posts? I noticed this in // TODO is this right?
public static function get_posts( $query = false, $PostClass = '', $return_collection = false ) {
add_filter('pre_get_posts', array('Timber\PostGetter', 'set_query_defaults'));
$posts = self::query_posts($query, $PostClass);
/**
* Filters the posts returned by `Timber::get_posts()`.
*
* There’s no replacement for this filter, because it’s called in a function that will be
* removed in the future. If you’re using `Timber::get_posts()`, you should replace it with
* `new Timber\PostQuery()`.
*
* @deprecated 2.0.0
*/
$posts = apply_filters_deprecated(
'timber_post_getter_get_posts',
array( $posts->get_posts( $return_collection ) ),
'2.0.0',
false,
'There’s no replacement for this filter, because it’s called in a function that will be removed. If you’re using Timber::get_posts(), you should replace it with new Timber\PostQuery().'
);
return $posts;
}Is the plan to make |
|
@acobster Yeah I think everything should use the Post Factory under the hood. Would be probably best to continue here so that we have all discussion here in this thread. |
Yeah we can remove the default contructor and just do everything in the factory. That would be the best I think. |
# Conflicts: # lib/PostGetter.php # lib/Term.php # tests/test-timber-menu.php # tests/test-timber-post.php # tests/test-timber-term.php
# Conflicts: # lib/Menu.php # lib/MenuItem.php # lib/Post.php # lib/PostCollection.php # lib/PostGetter.php # lib/QueryIterator.php # lib/Term.php # lib/TermGetter.php # lib/Twig.php
There was a problem hiding this comment.
Ok, so I tried take a closer look and tried to update some tests and quickly ran into a couple of questions.
First:
- Is the logic from #1793 (comment) already included in this pull request?
Posts/Terms
The following questions apply to both posts and perms, but I only ask them with the example of posts.
Get a post/term
I guess we agreed that the final API for getting posts is the following:
$post = Timber\Post::get( get_queried_object_id() );We also agreed that the get() method always needs a post ID or an object. If a post can’t be found, it will return null.
- Should the
get()method have a default ofnullfor the$post_identifieror would that fall away?
Currently, the get() method lies in the Timber\Factory\PostFactory class.
- Would we move that over to
Timber\Postor do we keep it in the factory and addTimber\Post::get()as a shorthand for the factory?
In the PostFactory class, there’s a get() and a get_object() method. The get() method creates a factory and then calls the get_object() method. As I understand it, the get() method is used to set the class with which to instantiate an object.
- Because
get_object()is public, I assume there’s a case where we callget_object()directly? When is that?
Get a post/term with a custom class
- How can I get a post with a custom class that extends
Timber\Post? Is it only possible through filters (#1793 (comment)), or also by passing in the name of a class as a parameter somewhere? Which filters can I use to change the classes used?
Get posts/terms
In #1793 (comment), @acobster asked whether we will still use this API:
$posts = Timber\PostQuery( $query );Or will it rather be like this?
$posts = Timber\PostQuery::get( $query );- What’s the most future-friendly way?
- Does
Timber\PostQuery()always need a parameter likeTimber\Post::get()does ? If yes, what’s the recommended way to get the current query (as an equivalent toget_queried_object_id()? - How can I get posts with a custom class that extends
Timber\PostQuery? - Which filters can I use to change the classes used?
Users
If we have the API with get() for posts and terms, I guess we will use the following API for users?
$term = Timber\User::get( $user_id );Timber\User::get() always needs a term ID or an object. If a term can’t be found, it will return null.
- Will a user class be added to
Timber\Factory\ObjectClassFactory? If yes, how will it be handled in theObjectClassFactory::get_class()method? Currently, the second parameter$object_typeasks for a post type or a taxonomy. How would it look like for user objects?
Get a user with a custom class
- How would I get a user with a custom class that extends
Timber\User? - Which filters can I use to change the classes used?
Comments
The same goes for comments:
$term = Timber\Comment::get( $comment_id );- Will a comment class be added to
Timber\Factory\ObjectClassFactory? If yes, how will it be handled in theObjectClassFactory::get_class()method? Currently, the second parameter$object_typeasks for a post type or a taxonomy. How would it look like for comment objects?
Get a comment with a custom class
- How can I get a comment with a custom class that extends
Timber\Comment? - Which filters can I use to change the classes used?
|
@gchtr great questions. I definitely don't know the answer to all of them. :)
Not all of it. I think there are still a few instances where the new API is still broken. (I want to try to get all the tests passing again, or at least the right ones failing, before I try to hack at the factory code any more. I haven't tried this since Jared's most recent 2.x merge.)
Not sure about these ones. @pascalknecht ? Having two seems like it would be confusing and unnecessarily complicated. I don't really see a compelling argument not have a
I'm not convinced that I will try to circle back to the other questions about PostQuery, Users, and Comments, but in short, I think the 1.x APIs for all of those things suffer from the same problems that Posts do, and should all be driven by Factories under the hood. |
|
@acobster makes sense. We should probably just have Post::get() and all related functions (Comment::get, etc.). We should then be careful to just let the users use these API's and hide the rest of the implementation such that they cannot be accessed. |
This replaces *most* instances of `new Timber\Post(...)` with calls to `Timber::get_post()`, which in the future will be the only way to instantiated Timber objects. The `Timber\Post` constructor will be made protected.
These were intended to test direct instantiation of Terms, which is going away.
|
Closing this, but work on Factories will continue in #2093. Thanks so much for your contributions to this effort, @joshlevinson ! |
|
Thanks for taking the charge and pushing the idea forward to reality! |
Merge of #1220. Feature will be developed further here.