When we started our community forums,
back in 2014, after a detailed research, we decided that XenForo
is a mature option which would help us provide a great experience to our community members.
As time passed, this proven to be the right choice, that goes to say how much good research can help.
We started with the 1.x version and we also added quite a number of modifications to it, for example,
when a new member registers, we need to make sure a valid product license is used,
we also need to check that members have a valid support pack when they post, or to be aware of members renewing their support pack over night.
Remember that we're selling MailWizz via Envato's Marketplaces, so we need to use their API to do all these checks, which adds a layer of complexity.
Add-on requirements
After the migration to XenForo 2.x, we needed to port some of these changes over, so we decided to write a add-on for this, which is the right way to extend XenForo.
We had the following requirements for the new add-on:
1. License check at registration
2. Cron job that runs daily and checks the validity of the support packs
3. When a new thread / reply is created, check the support pack and allow the action only if it's valid
To get started, we created a local development environment using Docker and installed XenForo.
As our IDE, we're using PHPStorm which helps a lot with code completion for XenForo classes, methods, traits, etc.
Once the above was done, we enabled the development mode for XenForo by editing
src/config.php
and adding:
$config['development']['enabled'] = true;This step is also described in XenForo's documentation.
According to the docs, you should start creating your add-on from command line by running XenForo's cmd.php
file:
php cmd.php xf-addon:createwhich will ask you a number of questions in order to create your add-on initial files, which are stored under
src/addons
.Here's the output we got for our add-on:
# php cmd.php xf-addon:create Enter an ID for this add-on: MailWizz/MailWizz Enter a title: Enter a version ID. This integer will be used for internal version comparisons. Each release of your add-on should increase this number: 10000 Enter the version string. e.g. 1.0.0 Alpha: 1.0.0 Does this add-on supersede a XenForo 1 add-on? (y/n) n The addon.json file was successfully written out to /var/www/web/src/addons/MailWizz/MailWizz/addon.json Does your add-on need a Setup file? (y/n) n If you change your mind, create a file named Setup.php in /var/www/web/src/addons/MailWizz/MailWizz which should extend AbstractSetup and optionally use one of the StepRunnerX traits.We used the id
MailWizz/MailWizz
because we want to keep all the functionality created by MailWizz under the same namespace.We can now navigate in XenForo's admin panel, to Add-ons menu to see and enable our new extension:
1. License check at registration
Of course, this is just an empty extension, it doesn't do anything special yet.
We can start by adding functionality to validate the license key at registration.
Our registration page looks like:
Here, the license key field is a user custom field defined in XenForo:
What's great related to XenForo's custom fields is that it allows you to define your own validation callbacks.
This means we can create our own PHP class which XenForo will call to validate the license key.
Inside our newly created add-on at src/addons/MailWizz/MailWizz
we can create a new folder called FieldValidator
and inside it a new file LicenseKey.php
where we will declare our validation class, thus the contents of LicenseKey.php
will be:
<?php declare(strict_types=1); namespace MailWizz\MailWizz\FieldValidator; use XF\Admin\App as AdminApp; use XF\CustomField\Definition; use XF\Entity\UserFieldValue; use XF\Mvc\Entity\ArrayCollection; /** * Class LicenseKey * @package MailWizz\MailWizz\FieldValidator */ class LicenseKey { /** * @param Definition $definition * @param mixed $value * @param mixed $error * * @return bool */ public static function run(Definition $definition, &$value, &$error): bool { // don't validate in admin area if (\XF::app() instanceof AdminApp) { return true; } /** @var ArrayCollection[UserFieldValue] $licenses */ $licenses = \XF::finder('XF:UserFieldValue') ->where('field_id', '=', 'license_key') ->where('field_value', '=', $value) ->fetch(); if ($licenses->count() > 0) { $error = 'This license is already in use!'; return false; } return true; } }Our class actually contains quite a bit more code, but it's not relevant to this example now, thus we don't show it.
In the above code, we're also using XenForo's finder to talk to the database.
All XenForo's entities(
UserFieldValue
in above example) used by the finder are stored in src/XF/Entity
so you can take a look there to see what's available to you. This is where using a good IDE such as PHPStorm will help a lot.Once the validation class has been created, we can tell XenForo to use it to validate our license key, therefore we go in admin panel and edit the custom field as follows:
Now each time when somebody tries to register, the validation class will be triggered and will validate the license.
2. Cron job that runs daily and checks the validity of the support packs
Next in our list is to add a cron job that runs daily and checks the validity of the support packs.
XenForo makes this relatively easy, remember that earlier, in order to create our add-on, from command line, we ran:
php cmd.php xf-addon:createTurns out that
xf-addon:create
is the ID of a console command, and we can create one in our add-on as well.XenForo uses Symfony's Console component for its command line commands and will try to autoload commands from the
src/addons/MailWizz/MailWizz/Cli/Command
folder, therefore, we will create the folders and put the command file inside.Our file is named
HandleLicenseSupport.php
and looks like:<?php declare(strict_types=1); namespace MailWizz\MailWizz\Cli\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use XF\Entity\User; use XF\Entity\UserFieldValue; use XF\Mvc\Entity\ArrayCollection; /** * Class HandleLicenseSupport * @package MailWizz\MailWizz\Cli\Command */ class HandleLicenseSupport extends Command { /** * @inheritDoc */ protected function configure() { $this ->setName('mailwizz:handle-license-support') ->setDescription('Update support packs for users'); } /** * @param InputInterface $input * @param OutputInterface $output * * @return int * @throws \XF\PrintableException */ protected function execute(InputInterface $input, OutputInterface $output) { /** @var ArrayCollection[User] $users */ $users = \XF::finder('XF:User') ->where('is_banned', '=', 0) ->order('register_date', 'asc') ->fetch(); /** @var User $user */ foreach ($users as $index => $user) { // ... } $output->writeln("Done"); return 0; } }Our class actually contains quite a bit more code, but it's not relevant to this example now, thus we don't show it.
We can see that in the
HandleLicenseSupport::configure
method we basically register the command, this means we will be able to run it from command line like:php cmd.php mailwizz:handle-license-supportwhich makes it easy to be added in a cronjob to run daily, for example:
0 0 * * * /usr/bin/php /var/www/web/cmd.php mailwizz:handle-license-support >/dev/null 2>&1The
HandleLicenseSupport::execute
method, as the name says, will get executed when you call the command and will run your code.
You can check out Symfony's Console component documentation to see more ways to customize these commands.3. When a new thread / reply is created, check the support pack and allow the action only if it's valid.
Lastly, we want that each time a member tries to post new content, to validate the support pack and allow the action only if the support pack is valid, otherwise show a meaningful error message.
XenForo has a number of events which are triggered at various execution times throughout the application.
When running in development mode, we can go to Admin > Development > Code event listeners and see exactly which event listeners are used so far and to which events they are attached to.
For our purpose, we will need to add a new event listener as follows:
which looks similar to when we added the validation callback for our license key custom field.
The entity_pre_save
event is triggered each time a entity is saved, for example when a new thread or post reply is created.
In the Execute callback
field we can see that we told XenForo to load the class MailWizz\MailWizz\Listener\CheckAccess
and call the run
method on it.
The class is located in src/addons/MailWizz/MailWizz/Listener/CheckAccess.php
and looks like:
<?php declare(strict_types=1); namespace MailWizz\MailWizz\Listener; use XF\Entity\UserFieldValue; use XF\Mvc\Entity\Entity; use XF\Entity\Thread; use XF\Entity\Post; /** * Class CheckAccess * @package MailWizz\MailWizz\Listener */ class CheckAccess { /** * @var bool */ public static bool $handled = false; /** * @param Entity $entity */ public static function run(Entity $entity): void { if (self::$handled) { return; } $request = \XF::app()->request(); if (!$request->isPost() && !$request->isXhr()) { return; } $classes = [ Thread::class, Post::class, ]; $found = false; foreach ($classes as $class) { if ($entity instanceof $class) { $found = true; break; } } if (!$found) { return; } $visitor = \XF::visitor(); if (empty($visitor) || empty($visitor->user_id)) { return; } if ($visitor->is_moderator || $visitor->is_admin) { return; } $supportValid = '....'; if ($supportValid) { return; } $entity->error('Your support has expired, please renew your support pack before posting new content!', null, false); self::$handled = true; } }Our class actually contains quite a bit more code, but it's not relevant to this example now, thus we don't show it.
While the above code will be triggered every time when a entity save happens, it does contain a few conditions to optimize it and make sure it gets triggered only when needed, thus the performance impact is negligible.
Add-on redistribution
In order to also redistribute this add-on, we will have to build it, which can be done from command line by running:
php cmd.php xf-addon:build-release MailWizz/MailWizzThe above command will create:
/src/addons/MailWizz/MailWizz/_releases/MailWizz-MailWizz-1.0.0.zip
that can be loaded in another XenForo application and get the same functionality we just built here,
the only exception being the field validator, which you'd have to add again in the XenForo admin panel.Final add-on structure
The final add-on structure looks like:/var/www/web/src/addons/MailWizz/MailWizz |-- Cli | `-- Command | `-- HandleLicenseSupport.php |-- FieldValidator | `-- LicenseKey.php |-- Listener | `-- CheckAccess.php |-- _releases | `-- MailWizz-MailWizz-1.0.0.zip |-- addon.json