Status
Not open for further replies.

XenFun

Staff member
Administrator
Joined
Apr 28, 2020
Messages
695
Reaction score
1,669
Points
103
Location
PHP

Reputation:

General concepts
The following sections go into detail about some of the general systems and concepts you will come across while developing a XenForo add-on. If you are familiar with XenForo 1.x development, then a lot of these concepts will seem familiar to you, though it's worth reviewing them as there are some excellent new tools and features to help you develop add-ons.

Vendor components
XF2 is not powered by a specific framework as XF1 was, however, we have employed the use of certain popular, well-tested, open-source packages to help with specific tasks. For example, we use a project named SwiftMailer for email sending and a project named Guzzle as an HTTP client. All third party projects are loaded from the src/vendor directory.

It is not currently possible for add-on developers to add their own dependencies to this location.

Integrated Development Environment (IDE)
Before starting work on XF2 development, you may want to spend some time evaluating the application with which you will actually be creating and editing PHP files. This is commonly referred to as an IDE. There are a number of options available ranging from basic Notepad to something like Sublime Text which can be expanded to have better PHP support with add-ons, up to a proper IDE, such as PhpStorm. Internally, we use PhpStorm as our preferred IDE. This is a premium and commercial product, but there may be free alternatives available. Either way, no one can tell you the best application for your requirements, and you should spend some time with a number of products (even free ones) and use that experience to find out your preference.

Autoloader
XF2 uses an autoloader which is automatically generated by Composer. This allows all XF code, third-party vendor code, and any add-on developer code to be automatically included throughout the entire project without having to include/require your classes manually.

The autoload root for all XF add-ons is the src/addons directory. This means that all of your class names will be relative to this base location. It's also worth noting that XF2 employs a strict "class per file" pattern to naming. Each file should only contain a single class, and the name of that class should identify the exact location of the class PHP file on the file system.

For example, if you want to create a new class in a file named src/addons/Demo/Setup.php (where Demo is your add-on ID) then this class will be named Demo\Setup. Conversely, if you had a class named Demo\Entity\Thing then you will know the file for this class is located in the path src/addons/Demo/Entity/Thing.php.

Namespaces
Throughout XF we use namespaces so that we can reference classes in the same namespace more succinctly. It is recommended that all add-ons also use namespaces. In the above example, we talked about a class named Demo\Setup. Using namespaces, the class would actually be named simply Setup but the namespace will be set to Demo. As a more concrete example, we also talked above about a class named Demo\Entity\Thing. Let's see what the PHP code would look like for this class:

PHP:
<?php

namespace Demo\Entity;

class Thing
{

}

If there was a class named AnotherThing in the src/addons/Demo/Entity directory, we could reference this class in the Thing class simply as AnotherThing because that class is in the same Demo\Entity namespace.

Short class names
Occasionally, classes referenced in XF are shortened. For example, if you wish to call the User entity (more on entities below) then you may see the class name referenced as simply XF:User. The use of short class names and the full class name they resolve to is entirely context-sensitive. Therefore, in the context of a call to an entity, the short class name will resolve to the following full class name XF\Entity\User. The XF part indicates the file path (based on add-on ID), the Entity part is implied by calling the entity and the User part indicates the specific entity. Similarly, when you start creating your own classes, you will also use short class names to reference your own classes. For example, if you need to create a new Thing entity for your Demo add-on, then you would write the following:
\XF::em()->create('Demo:Thing');
This would resolve to the Demo\Entity\Thing class. Similarly, if you wanted to access a Thing repository, you would write it as follows:
\XF::repository('Demo:Thing');

Notice how the short class names are identical. The repository call would actually resolve to Demo\Repository\Thing.

Extending classes
A great deal number of classes in XF2 are extendable which allows developers to extend and override the core code without having to directly edit it. If you're familiar with XF1 development, you will be somewhat familiar with the following process:

  1. Create a Listener PHP file
  2. Create a class which will ultimately extend the original class
  3. Write a function which matches the expected callback signature for one of the load_class events and adds the name of your extended class
  4. Add a "Code event listener" in the Admin CP which specifies the Listener class and method name for the function mentioned above, and optionally hint as to which class is being extended
In XF2 we have removed these events in favor of a specific system called "Class extensions". The process is as follows:

  1. Create a class which will ultimately extend the original class
  2. Add a "Class extension" in the Admin CP which specifies the name of the class you are extending and the name of the class which is extending it
This clearly cuts down on some of the boilerplate required to extend classes, and also provides a dedicated UI for viewing and managing these extensions. Let's look at the process by extending the public Member controller, and adding a new action that displays a simple message.

The first thing to do is to create an add-on. We previously outlined how to do that using the xf-addon:create command here. For this example, we'll assume you created an add-on with an ID and title of "Demo".

You will now have an addon.json file for this add-on in the following location src/addons/Demo/addon.json.


Note
Although, strictly speaking, you can place your extended classes wherever you like within your add-on directory, it is recommended to put extended classes in a directory which easily identifies a) the add-on the class belongs to b) the type of class being extended and c) the name of the class being extended. In the following examples, we are extending the public XF Member controller so we will place our extended class in the following path: src/addons/Demo/XF/Pub/Controller/Member.php.

The extended class needs to exist before we add the class extension to the Admin CP. So, follow the following instructions:

  1. Create a new directory named XF inside src/addons/Demo
  2. Create a new directory named Pub inside src/addons/Demo/XF
  3. Create a new directory named Controller inside src/addons/Demo/XF/Pub
  4. Create a new file named Member.php inside src/addons/Demo/XF/Pub/Controller
The initial contents of your PHP file should be as follows:
PHP:
<?php

namespace Demo\XF\Pub\Controller;

class Member extends XFCP_Member
{

}

If you're familiar with extending PHP classes generally, but not familiar with XF, the above example may initially seem confusing. The reason for that is you may have been expecting to extend the XF\Pub\Controller\Member class directly, rather than XFCP_Member. In XF we use the "XenForo Class Proxy" system (XFCP for short) to build an "inheritance chain" which ultimately allows a single class to be extended by multiple add-ons. The convention is to reference a dummy extended class which is the current class name Member and prefix it with XFCP_.

Now the class has been created, we can create the class extension on the Admin CP > Development > Class extensions > Add class extension page.

All you need to do is enter the base class name (XF\Pub\Controller\Member) in the first field, and the extended class name (which you just created) in the second field (Demo\XF\Pub\Controller\Member) and click the "Save" button.

Your class extension should now be active, but currently, not doing anything. To make something happen, we need to either override an existing method within this class by creating a method of the same name as an existing one or adding a new method entirely. Let's do the latter:
PHP:
<?php

namespace Demo\XF\Pub\Controller;

class Member extends XFCP_Member
{
    public function actionHelloWorld()
    {
        return $this->message('Hello world!');
    }
}

We talk more about controllers, actions, and replies in the Controller basics pages, so don't particularly worry about understanding this right now.

Now we've added some code to our extended controller, let's see it in action. Simply enter the following URL (relative to your board URL): index.php?members/hello-world. You should now see a "Hello world!" message displayed!

As mentioned earlier, it is also possible to override existing methods within a class. For example, if we changed actionHelloWorld() with actionIndex() then you would no longer have a "Notable members" list, it would instead display the "Hello world!" message! This isn't quite the right way to extend an existing controller action (or any class method, in fact) but we go into more detail about that in the Modifying a controller action reply (properly) section.

Type hinting
A lot of objects within XF are instantiated through factory methods. For example, if we want to instantiate a specific repository, we would write the following:
PHP:
$repo = \XF::repository('Demo:Thing');

This is a highly convenient and consistent way of instantiating an object. We know, just by looking at it, what object will be instantiated. The resulting code in that method knows how to return the correct object for what we've requested.

Unfortunately, however, your IDE probably has no clue (at least by default). As far as the IDE is concerned, this method will return an object instance of XF\Mvc\Entity\Repository. That's useful to a certain extent, but there are potentially lots of methods available in the specific Demo\Repository\Thing object which your IDE doesn't know about. This ultimately means that when you're trying to use your $repo object in the code, your IDE will not be able to make suggestions or auto-complete method names and the arguments it requires.

This is where type hinting becomes useful, and the syntax should be supported, by standard, by most IDEs and some "PHP aware" text editors. We would just change our repository call as follows:
PHP:
/** @var \Demo\Repository\Thing $repo */
$repo = \XF::repository('Demo:Thing');

The type hint above the repository call now tells the IDE that $repo relates to an object represented by the Demo\Repository\Thing class rather than the object it automatically inferred originally.

Type hinting is especially useful when extending classes, too. A potential problem with our class extension methods is that essentially your classes don't extend the original class you want to extend, but instead, this is proxied through a class that doesn't actually exist, e.g. XFCP_Member such as in the example above as Extending classes.

To rectify this issue, we automatically generate a file named extension_hint.php and store that in your _output directory.

This adds a reference that the IDE can read but PHP can't so that the IDE now understands that when we use $this inside any of the methods in this extended class that it can suggest and autocomplete methods and properties available in the Member controller or one of its parents.
 
Status
Not open for further replies.
Top