XF 2.2 Correct usage of addClassExtension to extend Templater class to support both XF2.1 and XF2.2?

mazzly

Well-known member
Hi,

We recently started seeing a compatibility problem with our own addon related to some AddonFlare addons, and the "workaround" at the moment is for our addon to set a higher execution order for the Templater extension. This doesn't feel like the correct solution...

I also have a feeling that this was not a problem before we added XF2.1 support to our addon by overriding the class extension in the app_setup event.

The errors can be seen here and here

Okay, so we have:

The class extension is setup normally as XF\Template\Templater -> MaZ\AMP\XF\Template\Templater under Development settings

Templater.php:
PHP:
class Templater extends XFCP_Templater
{
    // All the common functions for XF2.1 / XF2.2 
}

Templater21.php:
PHP:
class Templater21 extends Templater
{
    public function renderTemplate($template, array $params = [], $addDefaultParams = true)
    {
        // Same code as in Templater22, but with different func signature
    }
}
}

Templater22.php:
PHP:
class Templater22 extends Templater
{
    public function renderTemplate($template, array $params = [], $addDefaultParams = true, ExtensionSet $extensionOverrides = null)
    {
        // Same code as in Templater21, but with different func signature
    }
}
}

if our event listeners we have:
PHP:
public static function app_setup(\XF\App $app)
{
    if (\XF::$versionId < 2020000) {
        $templater_class = '\MaZ\AMP\XF\Template\Templater21';
    } else {
        $templater_class = '\MaZ\AMP\XF\Template\Templater22';
    }
    $app->extension()->addClassExtension('\XF\Template\Templater', $templater_class);
}

Questions:
1. Is app_setup the correct event to do the class extension "override"
2. Is $app->extension()->addClassExtension() the correct way to override the extension "during runtime"
3. Is there some better way to extend the class differently to support XF2.1 / XF2.2 at the same time without needing 2 differently released packages.
 
Last edited:
There is no reason to continue supporting XF 2.1 at this point. We don’t and many customers aren’t using it anymore. We don’t recommend trying to support both.
 
There is no reason to continue supporting XF 2.1 at this point. We don’t and many customers aren’t using it anymore. We don’t recommend trying to support both.
Sure I understand that, and probably at some point we will cut off as we also like to use the "bleeding edge". But saying that the previous version shouldn't be supported at all is a bit naive I think :)

Could you give some insight into my above questions though? (I feel like the questions are being dodged with the above answer)
For example what happens when 2.3 is released and it has another change in some Templater's function signature, should we just then say "nope 2.2 is old news", or is there some recommended way where we can set that changed function to be handled correctly across the 2.2/2.3 version?
 
Well, we don't support it and we no longer recommend customers who are using 2.1 to remain on it because if they need support, it simply won't be provided and we will no longer be providing updates for it. That's not naive, that's just a fact. 2.2 is not "bleeding edge" it is currently the only supported, stable version of the software.

If it's an existing add-on, then you already have a version that is compatible with XF 2.1. That should be the last version that is compatible with XF 2.1 and the next version should be compatible with only XF 2.2.

That's how we approach our add-on versioning, and it's how most developers are approaching their add-on versioning.

And, yes, the same applies in the future for any method signature changes.

As for whether your proposed solution would work, the answer is: I don't know. In theory, it should work and if it does then app_setup would be the code event to work with as that's the first one that fires. If it doesn't work, there's likely nothing more you can do.
 
Well, we don't support it and we no longer recommend customers who are using 2.1 to remain on it because if they need support, it simply won't be provided and we will no longer be providing updates for it. That's not naive, that's just a fact. 2.2 is not "bleeding edge" it is currently the only supported, stable version of the software.

If it's an existing add-on, then you already have a version that is compatible with XF 2.1. That should be the last version that is compatible with XF 2.1 and the next version should be compatible with only XF 2.2.

That's how we approach our add-on versioning, and it's how most developers are approaching their add-on versioning.

And, yes, the same applies in the future for any method signature changes.

As for whether your proposed solution would work, the answer is: I don't know. In theory, it should work and if it does then app_setup would be the code event to work with as that's the first one that fires. If it doesn't work, there's likely nothing more you can do.
Okay thanks! :)

Well to report: it "kinda works" at the moment, just seems to be problematic if our execution order is lower than some other addon that extends the Templater class... (and AddonFlare seems to set a crazy high 99999999 number there)

Going forward we will probably also do the "new version will get updates"-route, and old version at best will get some security fix :)
 
One other approach, and I’m not certain if it would work or not as I’m just theorising would be to have both of the template mods disabled and then enable the correct one on post upgrade via your setup script. Would definitely need some testing before going down that route though.
 
I use a different approach, I've written an aliasClass function which lets me generate a shim class, and redirect to which version I need to support. This is compatible with XFCP and functions which resolve back to the base class so I can do something like this;

Example; UserAlert.php;
PHP:
<?php

namespace SV\UserMentionsImprovements\XF\Repository;

\SV\StandardLib\Helper::repo()->aliasClass(
    'SV\UserMentionsImprovements\XF\Repository\UserAlert',
    \XF::$versionId < 2020000
        ? 'SV\UserMentionsImprovements\XF\Repository\XF2\UserAlert'
        : 'SV\UserMentionsImprovements\XF\Repository\XF22\UserAlert'
);

I use this pattern to support various XF versions and add-on which would normally break backwards compatibility
 
Okay, so it seems I have found the issue.

Problem was that Templater21 and Templater22 inherited from Templater, which inherits from XFCP_Templater.
(I'm guessing the XFCP system won't be able to "extend" the classes that are not directly inheriting from XFCP_Foo ?)

What I did now to get it working:
  • Rename and move the Templater21 into XF21\Templater and have it inherit from XFCP_Templater
  • Rename and move the Templater22 into XF22\Templater and have it inherit from XFCP_Templater
  • Extend with those classes inside app_setup-event
I also have a Templater-class for the common things, which is always loaded by default with a class Extension set in the admin development settings.

Structure now looks like:
1613052151616.webp

And now after the fix, the execution order doesn't matter anymore (which it shouldn't for my our needs)

Thanks @Chris D and @Xon for the insights :) 👍
 
I'm raising this thread from the dead now, since this finally came back to bite us in the ass :D

Luckily @digitalpoint was very helpful and gave us a solution that works together with other add-ons that extend the Templater class, and doesn't require using addClassExtension()

Below is how you can support different function signatures for the Templater class across multiple versions of XF

MaZ/AMP/XF/Template/Templater.php, which is extended in the normal way through ACP > Development Tools > Class Extensions
PHP:
if (\XF::$versionId < 2020000) {
    class Templater extends TemplaterAbstract
    {
        use \MaZ\AMP\Traits\Templater\XF21;
    }
} else {
    class Templater extends TemplaterAbstract
    {
        use \MaZ\AMP\Traits\Templater\XF22;
    }
}

abstract class TemplaterAbstract extends XFCP_Templater
{
    // Common code that works on both XF2.1 and XF2.2
}

MaZ/AMP/Traits/Templater/XF21.php:
PHP:
namespace MaZ\AMP\Traits\Templater;

trait XF21
{
    public function renderTemplate($template, array $params = [], $addDefaultParams = true)
    {
         // Do whatever we needed to here...
         return parent::renderTemplate($template, $params, $addDefaultParams);
    }
}

MaZ/AMP/Traits/Templater/XF22.php:
PHP:
namespace MaZ\AMP\Traits\Templater;

trait XF22
{
    public function renderTemplate($template, array $params = [], $addDefaultParams = true, $extensionOverrides = null)
    {
         // Do whatever we needed to here...
         return parent::renderTemplate($template, $params, $addDefaultParams, $extensionOverrides);
    }
}


Hopefully this helps someone in the future :)
 
The "magic" is in the fact that you can define the same class multiple times wrapped in normal PHP logic.

If you don't need to go down the road of traits (although they are fantastic), you can also just extend two different classes.

What I'm in the process of doing is abstracting out some of my classes so anything specific to XF is kept isolated in traits. In theory you could use the exact same class in XF1 and XF2 if you really wanted. What I'm doing is making it so identical classes can be used across completely different platforms (XF2 and WordPress).

For example a Repo class:

PHP:
<?php

namespace DigitalPoint\Cloudflare\Repository;

if (trait_exists('DigitalPoint\Cloudflare\Traits\XF'))
{
    use XF\Mvc\Entity\Repository;

    class Cloudflare extends \DigitalPoint\Cloudflare\Repository\Advanced\Cloudflare
    {
        use \DigitalPoint\Cloudflare\Traits\XF;
        use \DigitalPoint\Cloudflare\Traits\Repository\XF;
    }
}
elseif(trait_exists('DigitalPoint\Cloudflare\Traits\WP'))
{
    class Cloudflare extends \DigitalPoint\Cloudflare\Repository\Advanced\Cloudflare
    {
        use \DigitalPoint\Cloudflare\Traits\WP;
        use \DigitalPoint\Cloudflare\Traits\Repository\WP;
    }
}

abstract class CloudflareAbstract extends Repository
{
    abstract protected function getClassName($className);
    abstract protected function option($optionKey);
    abstract protected function updateOption($name, $value);
    abstract protected function getSiteUrl();
    abstract public function resolvePromises(array $promises);
    abstract protected function phrase($phraseKey, array $params = []);
    abstract protected function printableException($message, $code = 0, \Exception $previous = null);

    abstract public function getApiClass();

    protected $zoneIds = [];
    protected $endpointResults = [];

    protected $dashBase = 'https://dash.cloudflare.com';
    protected $teamsDashBase = 'https://dash.teams.cloudflare.com';

    // all my methods...
}

So now I can have a DigitalPoint\Cloudflare\Traits\XF trait that injects some basic XenForo-specific functions (things like getting a phrase, getting an option, updating an option, etc.), I can have a DigitalPoint\Cloudflare\Traits\WP trait that does the same thing, but for WordPress. The main class uses the abstracted methods so it will work on either platform.

Basically some trickery to allow class reuse across different platforms...
 
Top Bottom