Creating an Addon.

Lawrence

Well-known member
Part 1

This walk-through will take you step-by-step through the process of creating a working XenForo Add-on.

The Add-on created will allow Admins to prevent the editing of signatures for members with less than a specified amount of posts, and who belong to a particular group. To accomplish this we will need to set up a Listener, an Admin Options Page, and create the PHP files.

I decided to not use screen shots in this walk-through as I want to give the feel that you already know XenForo's Model View Controller (MVC) concept and are about to write your Add-on. Each step will be explained in detailed.

The working Add-on will be available at the end of this walk-through for coders to reference, or for licensed members to use.

If you plan on building the Add-on step by step you will need to enable Debug Mode. It is recommended that you first make a back-up of your config.php file before editing it. To enable debug mode:

open: yourForo -> library -> config.php

And add at the bottom: $config['debug'] = true;

At this time you may also want to add:

// $config['enableListeners'] = false;

Save your config.php file.

Creating the Add-on

Log into your AdminCP, and navigate to: Development -> Create Add-on

The following fields for this Add-on need to be filled with the following information:

Add-on ID: limitSigs
Title: Limit Signatures for new Members
Version String: 1.0
Version ID: 1

The Version ID is used internally to keep track of revisions. Increase this number for each revision.

The rest of the fields we do not need for this Add-on.

Click the Save Add-on button.

As the purpose of our Add-on is to not allow new members to edit (create) a signature, we must locate where members can edit their signature within XenForo, so we can Listen for the class being called. This is handled by a Controller. There are four Controller directories: Admin, Helper, Public, and Response. Editing a signature is not an Admin function. Nor is it a Helper, or a Response, that leaves us with Public. From your filemanager navigate to ControllerPublic. As editing a signature is part of a members account look for Account.php. Open this file and search for signature. The first word highlighted is just above: public function actionSignature(). This is the function we want to override.

Now we must listen for the class which this function belongs to be called (XenForo_ControllerPublic_Account).

XenForo comes with quite a few Listeners that we can choose from. Navigate to: Development -> Code Event Listeners. Click on the Create New Code Event Listener.

The first drop down allows you to select which event to listen to. As we want to listen to a class from the controller being loaded, from the drop down, select Load_Class_Controller.

The load_class_controller listener is tricky, but it is powerful and provides the most flexibility of all the Listeners. When you select this listener you will notice in the explanation of the load_class_controller that the class must be defined as: class My_Class_Name extends XFCP_My_Class_Name. This is true, but it can not be loaded directly. Loading this class directly will result in the AutoLoader to generate this exception: Cannot load class using XFCP. Load the class using the correct loader first.

What the error means is that the new class you want to create must be loaded before it can be resolved. But the question is, where does it get loaded, and how? This is the purpose of the $extend array argument.
 
Part 2

Leave the Listener page open, and using your file manager browse to: yourForo/library/

We will now set up our Add-on's directory structure.

Create a new directory called LimitSig

Notice the L and S in the directory name is upper case. We use this format to maintain the same standard as XenForo's directory names.

Create a new directory in LimitSig called Listener. Here is where we will store our listener. Within Listener, create a new file called LoadClassController.php. The directory and file name structure we are using will make it easy to locate files to be edited in the future, and it follows the same format as XenForo.

In your LoadClassController.php file add:

Code:
<?php

class LimitSig_Listener_LoadClassController
{
        public static function loadClassListener($class, &$extend)
        {

                if ($class == 'XenForo_ControllerPublic_Account')
                {
                        $extend[] = 'LimitSig_ControllerPublic_LimitSig';
                }

        }

}

Save LoadClassController.php

If you notice the class reflects the directory structure. This is very important. Our function contains two arguments: $class and $extend. $class will contain the class name that we will test for. $extend[] is an array that we will add to if $class contains XenForo_ControllerPublic_Account. Using this array allows other Add-ons to use this Listener without interference.

Now we can add our in our Class and Method in the Execute Callback text boxes of our Listener we are creating in the AdminCp -> Development section.

For Class type in: LimitSig_Listener_LoadClassController

The Method is our function. Type in: loadClassListener

Leave the Callback Execution Order defaulted. This value will only need to be changed if another load_class_controller event must happen before this one is executed.

Ensure Enable Callback Execution is checked.

In Description type in: Listens for XenForo_ControllerPublic_Account class

Select Limit Signatures for new Members for the Add-on drop down and click save. If you followed the structure above the Listener will be successfully saved.

The class in our LoadClassController.php file will now listen for XenForo_ControllerPublic_Account. When that class is loaded, $extend[] will be populated with the name of LimitSig_ControllerPublic_LimitSig. We now need to create that file and class. By looking at that class name, you can tell what directory it will be in, and the name of the php file that we need to create:

LimitSig -> ControllerPublic -> LimitSig.php

Create the ControllerPublic directory, and then LimitSig.php. In the LimitSig file add:

Code:
<?php

class LimitSig_ControllerPublic_LimitSig extends XFCP_LimitSig_ControllerPublic_LimitSig
{

    public function actionSignature()
    {

    }

}

You should notice the use of the XFCP_ prefix that was referenced in the listener description. As we can not extend a non existing class, loading it into $extend[] validates our class so it can now be extended.

Notice the function actionSignature(). That is the function in Account.php that we want to over ride. Within this function is where we will be adding our code to check for post count.

We can leave this function blank until we create a new options group in the AdminCP. If you are following along with each part, you can turn off this Add-on until we add in the code:

AdminCP home, and uncheck the Add-on Limit Signatures for new Members.
 
Part 3

To add options to the Limit Signature Add-on, navigate to AdminCP Home -> Options.

At the top of the page, click: + Add Option Group

Group ID (the options name that contains the values Admins set): limitsigOptions.

Title (the name that appears in the Options list): Signature Options

Description (a brief description that shows when you click the Option Group): Leave blank, unless you feel a more detailed description is needed.

Display Order: default

Add-on: Choose Limit Signatures for new Members from the drop down.

Click Save Option Group.

Now that you have your new group created, it's time to add a couple of options. The first option will allow Admins to set the minimum amount of posts a new member needs to make to be able to have a signature. And the second is to apply this minimum amount to a specific group, or groups. Adding the group option will allow us to select which groups should have the message count check applied to it, and which would be exempted, for example: a new Sponsor or Contributor with a message count less than that specified will still have the permission to create a signature.

Click: Add Option

The Add Option page has numerous text boxes, drop-downs, and check boxes that you can use to create an option. The first option is Option ID. This is the ID of the option we will be testing in our LimitSig.php file. Give it a name that will easily associate it with this Add-on, and what the option will contain. For this Add-on I chose: sigmessageCount. Take note of the case.

Add-on (Select from the drop-down): Limit Signatures for New Members

Title (the name of the option): Minimum Message Count

Explaination (a brief description of this option): Members minimum message count required to create a signature. Enter a value between 0 and 50. A setting of O turns the Minimum Message Count check off.

Edit Format. From the drop-down, select what type your option will be: a text box, spin box, On/Off Check Box, Radio Buttons, Select Menu, Check Boxes, Named Template, or PHP Callback. As we are looking for a number that we want to limit, select: Spin Box.

Format Parameters. You can set limits and behaviors to certain types of options. As we are using a spin box we can add to this option a min value, max value and a step value. Any value below the min, will default to the min value, and any value inputted above our max value will default to the max value. Just place each one you want to define on it's own line. The step value sets how much pressing the + or - will increase the value in the text box. Remove the step=x line if you want a step of 1.
Min=0
Max=50
Step=5

Data Type: The type of data you want set. Select Unsigned Integer from the drop-down. We do not want our data to be a positive integer as any value below 1 will default to 1, and we want to be able to use 0.

Default Value. This is the defaulted value set for an option when an Add-on is imported. As 5 is a good number for limiting users with less than x posts, we'll set the default value to 5.

Array Sub Options. As we are not storing our value in an array, we will leave this empty.

Validation Callback. Leave blank. This field will be used if you need to validate an Administrators selection. For example, if a forum an Admin selected is indeed a Forum and not a Category or a Page.

Display in Groups. This is a list of all Option Groups you want this option to appear. The default is the Add-on selected in the drop-down. Leave as set.

Include this Option in Backups. Default checked. Checked.

Press the Save Option button. You will now have your first working option in our Signature Options Group, that you can set and save.

Once again, Select Add Option.

Option ID: siglimitGroups

Add-on: Limit Signatures for new Members

Title: Apply to Groups

Explanation: Select which groups require a minimum message count to create a signature. Those members that are Administrators and Moderators that belong to one of the selected groups will be excluded.

Edit Format: PHP Callback. We are using PHP Callback as we want to generate a list of groups (with the exception of Unregistered, Administrative, and Moderating), that include those created by an Administrator.

Format Parameters. As we selected PHP Callback, we need to point the Option to where the Class and Method used is: LimitSig_Option_Group::renderOption

Data Type: Array. As Admins can select more than one group, an array will be required

Default Value: a:1:{i:0;s:1:"2";} The default value is set to Registered groups. How to change this value I'll explain when we have this option working as intended.

Array Sub-Options: * (we use an asterisk here, as we do not need to store information in specific named keys of an array, eg: width, height, user_id, etc.)

Validation Callback: default. As we are controlling what groups an Administrator can check, we know they will be valid.

Display in Groups: Signature Options.

Include this option in back-ups: Defaulted (checked)

Before we select Save, we need to create our PHP Callback Class and Method to render a list of groups for Admins to choose from.
 
Part 4

The Callback we will call to render the checkboxes for our option is called LimitSig_Option_Group. By looking at the name you should now be able to tell where the callback will be located and the name of the file itself. From your file manager, navigate to: Library/LimitSig. Create a new folder named Option, and within the Option folder create a new file: Group.php

Edit the file to add the Class and function:

Code:
<?php

    class LimitSig_Option_Group
    {

        public static function renderOption(XenForo_View $view, $fieldPrefix, array $preparedOption, $canEdit)
        {

            $preparedOption['formatParams'] = XenForo_Model::create('LimitSig_Model_GetUserGroups')->getUserGroupOptions(
            $preparedOption['option_value']
            );

            return XenForo_ViewAdmin_Helper_Option::renderOptionTemplateInternal(
            'option_list_option_checkbox',
            $view, $fieldPrefix, $preparedOption, $canEdit
            );

        }

    }

Select save.

This Class passes on the list of groups generated by our LimitSig_Model_GetUserGroups class to be rendered into checkboxes by the Admin Helper class. To view all available rendering options, take a moment to examine the Admin Helper Option file.

We now must create a new file for our LimitSig_Model. Create the folder Model/ within Library/LimitSig/ directory. Navigate in the Model folder and create GetUserGroups.php. Edit this file to add:

Code:
<?php

    class LimitSig_Model_GetUserGroups extends XenForo_Model
    {

    // returns a list user groups with the exception of Administrative, Moderating, and Unregistered

        public function getUserGroupOptions($selectedGroupIds)
        {

            $userGroups = array();
            foreach ($this->getAppropriateUserGroups() AS $userGroup)
            {
                $userGroups[] = array(
                'label' => $userGroup['title'],
                'value' => $userGroup['user_group_id'],
                'selected' => in_array($userGroup['user_group_id'], $selectedGroupIds)
                );
            }

            return $userGroups;

        }

        public function getAppropriateUserGroups()
        {

            return $this->_getDb()->fetchAll('
            SELECT user_group_id, title
            FROM xf_user_group
            WHERE user_group_id = 2 OR user_group_id > 4
            ORDER BY user_group_id
            ');

        }

    }

Select save.

This file will return a list of all User Groups that will be rendered into check boxes, with the exception of unregistered, Administrative, and Moderating. All new user groups you create will be listed in our Options as well.

To view the generated check boxes: AdminCP -> Options -> Signature Options

You should see Registered (defaulted to checked) and any new groups you may have created (which will not be checked), listed.

Setting the default value(s) for Check Boxes:

If you recall, we placed a:1:{i:0;s:1:"2";} as our default value, when we created the Apply to Group option. When you are creating multiple choice options, the easiest way to set which is selected as default is to leave the default value blank. Create the Callback, and then for the option, select which should be checked as defaults, and select save.

Open phpMyAdmin, select your XenForo data base, and click the xf_option table. Notice the row called default_value, it is stored as a BLOB, and it is located third over.

To see the contents of the default_value BLOB for siglimitGroup, click the Export tab. Under options located on the right side, scroll down and uncheck: Use hexadecimal for BLOB. Under Save as File, ensure Compression is set to None, and click Go. You should be given the choice open with:, select Note pad. If you are not given a choice, open the newly saved file with note pad, or equivalent. As the option was our last one created, scroll to the bottom of the text file. For our Addon, look for siglimitGroups, and you will see:

('siglimitGroups', 'a:1:{i:0;s:1:"5";}', 'a:1:{i:0;s:1:"2";}', 'callback', 'LimitSig_Option_Group::renderOption', 'array', '*', 1, '', '', 'limitSigs');

Remember the default value is third over. Highlight and copy: a:1:{i:0;s:1:"2";}. This is the value that you will paste in the Default Value of the Apply to Groups option.
 
Part 5

As our options are completed, we need to add in the code that will check a members post count and groups to determine if they can create a signature, or not. Open: LimitSig/ControllerPublic/LimitSig.php

Highlight:
Code:
    public function actionSignature()
    {

    }

and replace with:
Code:
    public function actionSignature()
    {

        if (!XenForo_Visitor::getInstance()->canEditSignature())
        {

            return $this->responseNoPermission();
        }

        else
        {
                $visitor = XenForo_Visitor::getInstance();

                $options = XenForo_Application::get('options');

                if ($visitor['message_count'] < $options->sigmessageCount AND
                   (!$visitor['is_moderator'] AND !$visitor['is_admin']))
                {

                            $applytogroups = $options->siglimitGroups;

                            if (empty($applytogroups))
                            {
                                $applytogroups = array(2);  // default it to Registered group
                            }

                            $belongstogroups = $visitor['user_group_id'];

                            if (!empty($visitor['secondary_group_ids']))
                            {
                                $belongstogroups .= ','.$visitor['secondary_group_ids'];
                            }

                            $groupcheck = explode(',',$belongstogroups);

                            unset($belongstogroups);

                            $sigcheck = true;

                            foreach ($groupcheck AS $groupId)
                            {

                                    if (in_array($groupId, $applytogroups))
                                    {
                                          $sigcheck = false;
                                          break;
                                    }

                            }


                            if (!$sigcheck)
                            {
                                        return $this->responseNoPermission();
                            }

                }

        }

        return $this->_getWrapper(
            'account', 'signature',
            $this->responseView(
                'XenForo_ViewPublic_Account_Signature',
                'account_signature'
            )
        );


    }

The first IF statement checks to see if the visitor has permission to edit their signature, and if not, exit right away as there is no point checking for post count, or group.

If the visitor does have permission, we now need to retrieve their post (message) count and all user groups they may belong to. We know if they can edit a signature, then they must be registered, so we can pull their user data:

$visitor = XenForo_Visitor::getInstance(); // load the member's user data

Now we need our Signature Option settings:

$options = XenForo_Application::get('options'); // initiate an instance of XenForo Options.

We can now compare a members post count to our Admin setting, and see if the member is a moderator or an administrator. If the member is a moderator or an administrator, or if their post count is not less than sigmessageCount value there is no point in checking what group they belong to.

If the member's post count is less, and the member is not a moderator or an administrator, we need to check if they belong to one of the groups that an administrator may have selected to be checked against.

$applytogroups = $options->siglimitGroups; // grab our addon's group settings

If $applytogroups is empty, we will default it to check for the Registered group. As members can belong to a primary group and numerous secondary groups, we will combine both into one array (if they are a member of a secondary group). A foreach is used to to test each members group with siglimitGroups. We can break on the first positive result, and return the no permission error. Otherwise, load the Signature editor.

If your Addon was turned off, be sure to turn it back on:

AdminCP home, and check the Add-on Limit Signatures for new Members


Exporting your Add-on

AdminCP Home -> Add-ons

From the Controls drop-down for Limit Signatures for new Members, select Export. This will save an XML document that will contain the Listener, and our Options group. If we had added any new templates or phrases, these would be exported as well.

Because we have files to include in our Add-on package, FTP to: yourForo/library/ and download the LimitSig/ directory to include all files and sub-directories.

Highlight the XML and the downloaded LimitSig/ directory and add to a zip file. Normally you would also create a readme text file that would contain: how to install, what the Add-on will do, available Options, a disclaimer, and a copyright notice.

The package is now ready for upload. Before you distribute your new Add-on, you should uninstall it from your forum, remove all files, and follow your install instructions to verify that it works.

The Limit Sig file is included below in zip format. Download and unzip, and follow the install instructions.

Feel free to add other options (such as strip images if trophy points is less than X, or even replace a members signature with an Admins choice if their Likes is below that of X).

As stated in the first post, this Add-on is for beta 1, and will be updated to reflect changes in furture versions of XenForo. The Add-on can be found in the Tutorial Addon.zip file.
 

Attachments

I've been following along for some time now. Thanks for posting this tutorial. It's truly very helpful, and I appreciate the work you put in to make this. :)

One error: Your post suggests that the first option should be named 'limitsigmessageCount', when your code uses 'sigmessageCount' instead.
 
I've been following along for some time now. Thanks for posting this tutorial. It's truly very helpful, and I appreciate the work you put in to make this. :)

Thanks, :) I'm hoping this tutorial will be helpful for those members who want to try creating a XenForo Add-on a try.

One error: Your post suggests that the first option should be named 'limitsigmessageCount', when your code uses 'sigmessageCount' instead.

Fixed, and thanks. I had the original version installed when I wrote the new Add-on for this tutorial, and incorrectly used the older options group as reference.
 
Thank you for taking the time and post this. I am sure this will come in handy to a lot of coders and help them with understanding xenforo and creating mods. It is very well written and detailed :)
 
Nice job.

I'd also suggest renaming LimitSig_ControllerPublic_Listener to something else, such as LimitSig_Listener, or even better LimitSig_AddOn as it's not a ControllerPublic and naming it as such infers a relationship/requirement/convention that doesn't exist.

The LimitSig_AddOn class could also be used to provide the install and uninstall methods of the addon, as well as any additional listener callbacks that may be added at a later date.
 
Nice job.

I'd also suggest renaming LimitSig_ControllerPublic_Listener to something else, such as LimitSig_Listener, or even better LimitSig_AddOn as it's not a ControllerPublic and naming it as such infers a relationship/requirement/convention that doesn't exist.

The LimitSig_AddOn class could also be used to provide the install and uninstall methods of the addon, as well as any additional listener callbacks that may be added at a later date.

Thanks, :)

I placed the listener in that directory because it is extending this class: XenForo_ControllerPublic_Account. Having the listener.php file in ControllerPublic directory of LimitSig tells me at a glance that it is listening for a specific class in XenForo_ControllerPublic to be loaded in $class. My memory isn't the greatest, so it works for me, :)

There are no DB alters, so there is no need for an install or uninstall function, the xml file uploaded suffices.
 
I was really meaning from a reader's point of view. XenForo, Zend and many other frameworks that follow the class -> file convention also follow a naming standard. The class name is in the form:

Code:
lib_type_implementation

This is useful because I can then learn things about the structure of the application without looking at the code. I know that everything in ControllerPublic/ is a ControllerPublic; everything in Model is a Model/; etc.

This approach breaks that convention and implies to someone looking at the file structure that SigPic includes a new controller, which isn't actually true.

This is a very good tutorial and I'm sure it will be a starting point for many people, so I would hate to see this practice picked up by a lot of people as it will cause confusion.

Besides, if you had all your listeners in one place there'd be nothing to remember ;)

Code:
/**
 * AddOn for MyAddOn.
 *
 * @package MyAddOn
 * @author john.smith@example.com
*/
class MyAddOn_AddOn
{
	/**
	 * Controller overrides.
	 * @see XenForo_Application::resolveDynamicClass()
	 *
	 * @param string $class Name of original class
	 * @param array $extend In list of classes that will extend the original
	 */
	public function loadClassController($class, &$extend)
	{
		if ($class == 'XenForo_ControllerPublic_Thread')
		{
			// do that magical thing to threads
			$extend[] = 'MyAddOn_ControllerPublic_Thread_MyThread';
		}
		else if ($class == 'XenForo_ControllerPublic_Forum')
		{
			// do that other magical thing to forums
			$extend[] = 'MyAddOn_ControllerPublic_Forum_MyForum';
		}
	}

	/**
	 * Model overrides.
	 * @see XenForo_Application::resolveDynamicClass()
	 *
	 * @param string $class Name of original class
	 * @param array $extend In list of classes that will extend the original
	 */
	public function loadClassModel($class, &$extend)
	{
		// extend the user model to include that extra information we need
		if ($class == 'XenForo_Model_User')
		{
			$extend[] = 'MyAddOn_Model_MyUser';
		}
	}
}
 
Thanks, Darren for your guidance, and more importantly your explaination. :) I will be updating this tutorial with the changes for the listener. As I'm new to the MVC approach I have a lot of learning to do, and have learned a lot since this tutorial was written.

Edit: Tutorial updated, and new zip uploaded.
 
All tutorial posts liked :) ... Helped me out on how to create and use admin options :)
 
Finished this tutor. Very helpful.
Thanks.

One question: Can I customize the error message or forward to other page or popup the login window?

Thanks.
 
I'm getting this error.

PHP:
Server Error

No controller response from XenForo_ControllerPublic_Account::actionSignature

XenForo_FrontController->_handleControllerResponse() in XenForo/FrontController.php at line 311
XenForo_FrontController->dispatch() in XenForo/FrontController.php at line 132
XenForo_FrontController->run() in /home/darkfxco/public_html/xenforo/forum/index.php at line 15

I've looked through everything.
 
I figured that out. Just can't quite narrow it down. Since I used followed this, I would guess that the problem is in LoadClassController.php or LimitSig.php
Should you point out at which step this error happened?

Part 2

Leave the Listener page open, and using your file manager browse to: yourForo/library/

We will now set up our Add-on's directory structure.

Create a new directory called LimitSig

Notice the L and S in the directory name is upper case. We use this format to maintain the same standard as XenForo's directory names.
 
Top Bottom