Entities and finders
Entities are XF2's version of data writers. However, they're much more extensive, as:
Most of these are generic finder methods, but unreadOnly() is a thread-specific method that applies logic based on the current visitor and complex conditions to limit the results to only unread threads. While XF1 had some similar concepts (implemented very differently), the complex restrictions weren't generally re-usable and that cause a lot of repeated code. In XF2, these constraints are generally shared by the finder object and can be reused. As an example, it's trivial to display only threads that the user has posted in but are also unread.
There's a lot more to this topic. It's a major change to the fundamental data access layer of XenForo and is probably one of the most important concepts to grasp in XF2. As we have other things to cover, let's just add a couple bullet points:
Service oriented system
Most complex actions have now been moved to service objects that represent that action only. Most services work in a "setup and go" type of approach; you setup your configuration and then call a method to complete the action.
This example was posted in the development update, but it's a good example:
Entities are XF2's version of data writers. However, they're much more extensive, as:
- They are used for reads and writes. Most work you do with data will be through an entity.
- They automatically handle encoding and decoding of values. For example, if you define a column as JSON, you just give it the original data structure (probably an array) and it will encode on save. Similarly, when the record is fetched, the value will be decoded back to that array when you access it.
- They can have getters to wrap logic around grabbing a value that isn't necessarily stored in the DB. For example, option titles are actually phrases, so there's no title column in the option table. However, we can call $option->title or $option['title'] and it will return a phrase object.
- They define their relations and they can be dynamically loaded as needed. $post->Thread->Forum will return the forum that the post belongs to, regardless of whether you included the thread and/or forum records when you loaded the post.
- They support some amount of horizontal reuse through the "behavior" system. This currently only adds extra behaviors when write actions are taken, but examples are to automatically adjust likes when content is deleted or hidden and applying change logging to an entity.
- Other methods can be added to expose whatever other functionality you need. Call $user->authenticate($password) to see if the password is correct or $thread->canView() to determine if the current visitor can view the thread.
PHP:
$finder = \XF::finder('XF:Thread');
$finder->where('node_id', $nodeId)
->with(['Forum', 'FirstPost'])
->unreadOnly()
->order('last_post_date', 'desc')
->limitByPage($page, $perPage);
$threads = $finder->fetch();
Most of these are generic finder methods, but unreadOnly() is a thread-specific method that applies logic based on the current visitor and complex conditions to limit the results to only unread threads. While XF1 had some similar concepts (implemented very differently), the complex restrictions weren't generally re-usable and that cause a lot of repeated code. In XF2, these constraints are generally shared by the finder object and can be reused. As an example, it's trivial to display only threads that the user has posted in but are also unread.
There's a lot more to this topic. It's a major change to the fundamental data access layer of XenForo and is probably one of the most important concepts to grasp in XF2. As we have other things to cover, let's just add a couple bullet points:
- When fetching multiple entities, a collection object is returned rather than a bare array. You can get the array if you like, but the collection object has various helper methods (pluck, filter, groupBy, etc).
- Relationships in entities can return many entities. To take a specific example, a thread may be watched by many people, but we generally only care about one person at a time. $thread->Watch[$userId] will return the watch record for the specified $userId. It won't eager load all of the other watchers. The "Watch" value is a "sparse" collection of entities that loads individual records on demand (or you can tell it to fetch all of them).
- You can eager load a single record of a "to many" relationship: $finder->with("Watch|$userId").
- Finders can be built in any order. You can apply a condition after you set the order or limits.
- You don't need to manually create a finder in every case. We have common methods in "repository" objects that setup finders for base/common cases. These repositories can also hold other logic/actions relating to a type of entity. Some examples include methods like countUnreadThreadsInForum(\) or rebuildThreadUserPostCounters(). While these aren't meant to be a generic dumping group, they can also be used for various helper methods, particularly when they manipulate the database directly or don't relate to a single entity.
- Most of our finder-related repository methods return the finder rather than the results. This means that they can be manipulated for specific cases or extended to add extra behaviors. For example, findThreadsForForumView() could be extended to always include a specific relation (though there is another place this could be done within the thread finder).
Service oriented system
Most complex actions have now been moved to service objects that represent that action only. Most services work in a "setup and go" type of approach; you setup your configuration and then call a method to complete the action.
This example was posted in the development update, but it's a good example:
PHP:
/** @var \XF\Service\Thread\Mover $mover */
$mover = $this->app()->service('XF:Thread\Mover', $thread);
if ($options['alert'])
{
$mover->setSendAlert(true, $options['alert_reason']);
}
if ($options['notify_watchers'])
{
$mover->setNotifyWatchers();
}
if ($options['redirect'])
{
$mover->setRedirect(true, $options['redirect_length']);
}
if ($options['prefix_id'] !== null)
{
$mover->setPrefix($options['prefix_id']);
}
$mover->move($targetForum);