XF 2.2 What's new for developers in XF 2.2?

Screenshot 2020-08-02 at 09.39.53.webpThe previous "have you seen" threads for 2.2 have generally been focused on what the changes mean to admins and end users. While we have made reference to some of the internal technical elements and what add-ons can do with these features, today we want to do a deeper dive into some of these changes from a development perspective.

What we're going to cover today is not an exhaustive list. No doubt you'll find other changes scattered throughout. Today we're just going to focus on some of the more significant or noteworthy changes.

So let's go!

Template extensions

This is a big part of the underlying changes that allowed us to implement the forum and thread type system. To sum this concept up in a sentence, it's an inverted template include. I know, that doesn't make sense but hopefully it will after some examples.

<xf:extends template="thread_view" />

<xf:extension name="above_messages">
   <xf:if is="$poll">
      <xf:macro template="poll_macros" name="poll_block" arg-poll="{$poll}" />

This happens to be the thread_view_type_poll template, which is what is used when viewing a poll thread. It's one of the simplest thread type templates.


The first line is one of the most significant. This marks the current template as extending thread_view (the standard template used when viewing a thread). When this line is present, it essentially means that we output the named template instead (though there's more to it :)). If we had nothing else in the template, this page would look exactly like a basic thread.


This is where most of the magic happens. You might be able to see where this is going. In this example, we've provided some content for the above_messages extension point.

To understand what this means, we need to look at part of the thread_view template itself. The relevant section:
<xf:macro template="lightbox_macros" name="setup" arg-canViewAttachments="{$thread.canViewAttachments()}" />

<xf:extension name="above_messages"></xf:extension>
<xf:ad position="thread_view_above_messages" arg-thread="{$thread}" />

If you compare this to the 2.1 version, it'll look pretty familiar. We had a poll reference here before and now we have the <xf:extension> tag.

Within the thread_view template, we're effectively marking this as an overrideable extension point. By default, it won't display anything, but another template can choose to change what is displayed in this location. Since the poll template is overriding this position, we'll get the poll displayed above the messages.

This is only one way to use <xf:extension> tags. In some cases, the default template might contain some content. A good example of this is the extended article forum example. In forum_view we now have something like this:
<div class="block-body">
   <xf:extension name="thread_list">
      <div class="structItemContainer">
        <!-- all of the standard stuff to display  sticky threads, normal threads, etc -->

Then in forum_view_type_article we can override that like this:
<xf:extension name="thread_list">
   <xf:if is="$forum.type_config.display_style == 'expanded' AND $forum.canViewThreadContent()">
      <!-- display extended versions of articles instead of the standard thread list -->
   <xf:else />
      <xf:extensionparent />

The new thing to note here is the <xf:extensionparent> tag. This allows you to output the parent/original version of the extended area. In this case, we're using it as a fallback to the original display when extended mode isn't enabled, though it can also be used to add things at the beginning or end of the area or to wrap the output.

Macro extensions

Macros can also be extended in the same way. The child macro just needs to be defined like this:
<xf:macro name="answer" extends="post_macros::post">...</xf:macro>

From there, you define extensions roughly as shown above.

Advantages over includes and macros

Template extensions aren't a replacement for includes or macro calls. They provide a more structured approach for situations where you want to replace or add functionality based on a more specific context.

Notably, marking up an area as an extension point doesn't require significant changes to the parent template. In most cases, an empty extension point can be added with virtually no interaction with the surrounding code or a "wrapped" extension point can be added simply by indenting the related code once more. Without this system, the code in question would likely become a macro, which would likely be moved to a different location in the template (or a different template entirely) and may introduce variable scoping issues, making it harder to change the functionality.

Other notable bits

  • <xf:extension> tags can also be defined with a value attribute which can allow them to be used for more than just HTML blocks, similar to the two approaches to <xf:set>.
  • There's also an <xf:extensionvalue> tag and extension_value() template function. These allow you to reference an extension multiple times if needed. The template function also allows you to use an extension with specific HTML attributes. We use this in several places to changes the classes applied to an element.
  • <xf:extensionparent> can also take an extension name to render the parent version of that. This is unlikely to be used often but there are some particular cases that are difficult without it.
  • When calling a macro, we now generally prefer <xf:macro name="template_name::macro_name"> over <xf:macro template="template_name" name="macro_name"> although both options are supported. The single attribute approach makes it more straightforward to dynamically switch to a different macro, something done quite commonly in the forum and thread types system.
  • We now enforce a-z, 0-9, and _ restrictions for macro names properly. Due to the fact that we didn't do this before, it's possible that add-ons/customizations have added improperly named macros. Therefore, we only enforce this in development mode and not in the install app, to prevent an upgrade from being blocked.

Forum and thread types

From a development perspective, it's important to consider forum and thread types as separate systems with a bit of interaction and encompassing similar ideas. However, they are totally separate systems and there's nothing to say you can't define a forum type without defining any unique thread types and vice versa.

Though there are some exceptions to this rule, when viewing a forum, you're dealing with a forum type handler and when viewing a thread, you're dealing with a thread type handler.

We're not going to go into every method these type handlers cover in this thread; we're just going to go over some concepts that relate to each. The abstract handlers have full phpDoc blocks that try to explain how the methods are used and in some cases, things to keep in mind.

Array validators

Before we dive into forum and thread types, it's worth noting the new XF\Entity\ArrayValidator class. This is designed to add simple entity-like validations to a basic array, including things like types, constraints, required fields, and validation callbacks. Both forum and thread types use this for their type config/data columns to centralize data validation.

Basic setup mirrors entity column definitions:
protected function getTypeConfigColumnDefinitions(): array
   return [
      'display_style' => ['type' => Entity::STR, 'allowedValues' => ['full', 'expanded']],
      'expanded_snippet' => ['type' => Entity::UINT],
      'expanded_per_page' => ['type' => Entity::UINT],

Forum types

Forum types add an additional type_config field to forums where you can store, well, forum-type configuration options. The defaults are setup in the getDefaultTypeConfig() handler method. When accessing this via the entity, we merge the defaults with any overrides, so you can be sure that each config option is present, which helps avoid unexpected errors when new config options are added.

Setting up custom type config options involves defining an admin template to contain those options, along with a method to handle reading the config from input and validating it. Best practice would also be to expose these options to the REST API, so there are methods to both expose the configuration when the forum data is returned and to update the config itself.

It's worth mentioning that a forum's type can be changed. The change process involves setting up the new type config options before the forum's type has actually changed. This is important because changing a forum type may change the type of the threads within the forum and that can be affected by the type config. (It is possible to block changing out of a specific forum type via the handler, if needed.)

Each forum type also defines the allowed thread types within that forum. This will affect the UI when creating a thread. If you want to add behaviors when viewing the forum that depend on the thread being a particular type, then you would likely want to make your forum only accept a single thread type. (Note that redirect threads are always allowed, so that is something you may need to account for.) This is done via:
  • getDefaultThreadType - defines the type that will be selected by default if there are multiple creatable types and the type that will be used if the thread is created in a manner that doesn't explicitly set the type.
  • getExtraAllowedThreadTypes - any additional thread types that may be allowed.
  • getCreatableThreadTypes - potentially a subset of the first two methods, this differentiates the types of threads that users can create manually vs those that can only be created internally. As an example, resource discussion threads would be allowed in discussion forums but end users can't manually create them.
The majority of the remaining methods relate to controlling the display and behavior of the forum view pages. Some of the things you can do include:
  • Override the display entirely. This is the nuclear option, but if you don't want to use any of the controller or display code from a default forum, you have that option.
  • Override the name of the view or template (forum_view) and manipulate the parameters sent to the template.
  • Override some of the macros used, such as for displaying thread list items or quick thread, and optionally pass additional arguments to the macros.
  • Add additional ways to filter the threads shown or to customize the available sort orders.
  • Adjust the thread list finders and to fetch additional content with the threads.
  • Customize the number of threads shown per page. (This is primarily for situations like the expanded article view, where you're going from a very small amount of content per thread to a large amount.)
In conjunction with the template extension system, these options give you the ability to heavily customize how a forum looks and works, while being able to use the existing code and default output where you want it. It also means that unless you change a particular area, any add-ons that change the output or additional behavior will still applied.

Thread types

Many of the concepts we just explained under forum types apply to thread types as well. However, some times the functionality differs.

Similar to forum types' type_config column, thread types expose type_data. This may expose configuration options, but it may also include additional data that's specific to that thread type. For example, with questions, we store the post ID and user ID for the solution in this data (though we do also mirror this to a separate table to aid certain tasks). However, some thread types may involve significantly more complex data that you might not want to store in the thread record. Our type-specific data management methods are designed to handle this.

Thread type data is displayed via the renderExtraDataEdit method. Note that this is used both in thread creation and thread editing contexts. Information about the context is passed into the method, allowing you to react appropriately. For example, while polls can be created with the thread, there are different constraints on editing an existing poll, so we don't show anything unless we're in the create context.

When processing the extra data, there are two approaches: simple and service. Simple processing is essentially the same as what we saw in forum types. We use the array validator system to map inputs to the extra data we want to store. The service approach allows you to create a service object to setup the actions you want to take. This will be used in conjunction with the base service (thread create, edit, etc) to ensure that no action is taken unless both services validate and once the base service's action has run, the type data service will execute as well. This is wordy, but it's roughly equivalent to how we handled poll creation in 2.1 but in a more generic way.

In terms of thread viewing, thread types introduce two new concepts:
  • Pinned first post - if set for the thread type, the first post of the thread will be pinned to the top of each page. This is integrated directly into thread_view so simply toggling this on will make it work, though there are some additional overrides that allow you to easily change the display of just the pinned first post.
  • Highlighted posts - these are an arbitrary number of additional posts that will be fetched on each page. You're responsible for controlling how and where they will be displayed. This is how the solution is handled in questions, but there are virtually limitless ways this can be used to enhance threads.
Beyond that, thread types allow you to override a number of things in a similar way to forum types:
  • Override the thread display entirely.
  • Override the name of the view and template used and manipulate the parameters sent to the template.
  • Override some of the macros used, such as for displaying posts, deleted posts or the pinned first post, and optionally pass additional arguments to the macros.
  • Apply custom filters to thread viewing. While this isn't currently being used out of the box, it made sense to bring the concept of filters to threads so that we could apply the necessary behavior changes when a thread is filtered.
  • Allow custom sort orders and control the default order. Note that while it is possible to change the default order of a thread to be something other than chronological, chronological sorting will always be available and there are situations where we will automatically switch to it. There are also caveats that non-chronological orders will disable some standard behaviors, most notably thread read marking.
  • Adjust the post list finder and to fetch extra content for individual posts.
  • Control when thread or post voting is supported and the related permission checks.
Thread types also define extension points for various thread and post lifecycle events, including:
  • Thread pre-save, post-save and post-delete.
  • Thread is made visible or hidden.
  • Thread enters or leaves the type.
  • Thread's counters are rebuilt.
  • Threads are merged into the thread.
  • A visible post is added to or removed from the thread.
  • A post in the thread is saved or deleted.
While there is significant power in both of these systems already, we still have ideas for additional places where these systems can hook into XenForo to provide even more flexibility. We're very excited to see what can be done with these systems!

Content voting

Content voting is a basic up/down voting system. The meaning for each vote is content specific, though an upvote represents something good and a downvote represents something bad. This system is distinct from reactions in that a positive reaction does not necessarily mean that the content deserves an upvote -- a funny post does not necessarily make a good answer to a question (indeed, you could get multiple "haha" reactions but have an overall negative score).

While we have mentioned this in the context of threads and posts thus far, like many of our systems, content voting is implemented through a handler system so it can be applied to other content types.

Largely this is done through implementing a simple handler object, applying a trait to your entity and implementing a few abstract methods, adding a couple of columns to the related table in the database, and setting up the UI/controller code to call into the controller plugin. From there, you'd just need to figure out how to make use of this info, perhaps by exposing a vote-based sorting option.

The system exposes separate concepts for voting being supported vs the user having permission to vote and downvotes being supported vs the user having permission to downvote. In threads and posts, we proxy these calls into the thread type handler to allow them to be controlled individually by type.

Other assorted changes of note

  • While we did mention this earlier, XenForo 2.2 will now require PHP 7.0. This means that we have started to use scalar type hints and return type declaractions where appropriate. (Note that nullable and void types are part of PHP 7.1 so they are not currently used.) At this time, we are not retroactively applying scalar or return type hints to existing code due to the number of backwards compatblity issues this could cause.
  • While we have not entirely dropped support for IE11, it has now been formally deprecated and visitors using it will receive a banner recommending they upgrade to a more recent browser. We will be dropping support for IE11 in a future release.
  • We have migrated any Zend Framework components we use to Laminas. Generally this is just a name change, but custom code that uses these components will need a small update. Read more about this in our Zend migration backwards compatibility break announcement.
  • Though we're not going to go into detail here, the activity summary email is also built on top of a handler system to make it easy for add-ons to add custom sections.
  • Similarly, the writing before registering system is built atop a generic "pre-registration action" handler system.
Template extensions are very interesting indeed, makes for much cleaner ways of adding custom content to templates and hopefully fewer template modification conflicts.
Worth mentioning that the templates do need to include the extension points, so template modifications will always be required at some point, though you can potentially make a modification to include an extension point instead. :)
And how will the extension be displayed if 2 add-ons use one extension?
That's probably not entirely relevant. The usage of this would primarily be for an add-on to use an extension point in its own template.

If you really do run into multiple add-ons trying to add extension overrides into the same template, it's a compilation error to define an extension twice. (It's nonsensical to allow this really. It would be totally ambiguous.)
Worth mentioning that the templates do need to include the extension points, so template modifications will always be required at some point, though you can potentially make a modification to include an extension point instead. :)
That raises a good point actually; how liberally are you going to apply template extension points? If I was to dig through some of my existing template mods, I'm sure I'd find some really ugly ones (regexp capturing a whole <xf:radiorow> f.ex. because I wanted to add the option after said row and it's the last option in a section). These bits could do with either a template modification capture comment, or an extension point.

Will we be able to report such instances and would you consider adding them as part of the beta process?
Actually, I thought about writing off about this half a year ago, and in fact it is very convenient to build in my additions, where you need to change the value of the output in 1 place. Waiting for Beta XF 2.2
To understand extensions better: template modifications will not need a find/replace, if using an extension? Or, am I in left field? Can I replace a template modification with a template that uses an extension? (sorry for the questions, just want to understand this).

Also how many <xf:extension name="xxxxx"></xf:extension> is there per top level template?
Top Bottom