Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 39 additions & 37 deletions docs/devs/lets-build-an-add-on.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,16 +370,16 @@ Finally, click save to save your template modification. If all has gone well, wh

We have our column, we have a UI to pass an input to that column, now we have to handle saving data to that column. We will do this by extending the Forum controller and extending a special method which is called when a node and its data are saved. First, let's create a "Class extension" which can be found under the "Development" entry in the Admin CP. Click "Add class extension".

Here we need to specify a "Base class name" which is the name of the class we are extending, which in this case will be `XF\Admin\Controller\Forum`. And we need to specify a "Extension class name" which is the class which will extend the base class. Enter this as `Demo\Portal\XF\Admin\Controller\Forum`. We should create that class before clicking Save.
Here we need to specify a "Base class name" which is the name of the class we are extending, which in this case will be `XF\Admin\Controller\ForumController`. And we need to specify an "Extension class name" which is the class which will extend the base class. Enter this as `Demo\Portal\XF\Admin\Controller\ForumController`. We should create that class before clicking Save.

Create a new file in `src/addons/Demo/Portal/XF/Admin/Controller` named `Forum.php`. This might seem like quite a long path, but we recommend a path like this for extended classes. It enables you to more easily identify the files that represent extended classes by virtue of the fact that they are in a directory of the same name as the extended "add-on" ID (in this case `XF`). It also makes it clear exactly which class has been extended as the directory structure follows the same path as the default class. The contents of the file should, for now, look like this:
Create a new file in `src/addons/Demo/Portal/XF/Admin/Controller` named `ForumController.php`. This might seem like quite a long path, but we recommend a path like this for extended classes. It enables you to more easily identify the files that represent extended classes by virtue of the fact that they are in a directory of the same name as the extended "add-on" ID (in this case `XF`). It also makes it clear exactly which class has been extended as the directory structure follows the same path as the default class. The contents of the file should, for now, look like this:

```php title="src/addons/Demo/Portal/XF/Admin/Controller/Forum.php"
```php title="src/addons/Demo/Portal/XF/Admin/Controller/ForumController.php"
<?php

namespace Demo\Portal\XF\Admin\Controller;

class Forum extends XFCP_Forum
class ForumController extends XFCP_ForumController
{

}
Expand All @@ -389,7 +389,7 @@ See [Extending classes](general-concepts.md#extending-classes) and [Type hinting

Click save to save the Class extension. Now we can add some code. The particular method we need to extend is a protected function called `saveTypeData`. When extending any existing method in any class, it is important to inspect the original method for a couple of reasons. The first being we want to make sure the arguments we use in the extended method, match that of the method we're extending. The second being that we need to know what this method actually does. For example, should the method be returning something of a particular type, or a certain object? This is certainly the case in most controller actions as we mentioned in the [Modifying a controller action reply (properly)](controller-basics.md#modifying-a-controller-action-reply-properly) section. However, although this method is within a controller, it isn't actually a controller action itself. In fact, this particular method is a "void" method; it isn't expected to return anything. However, we should always ensure we call the parent method in our extended method so let's just add the new method itself, without the new code we need to add:

```php title="src/addons/Demo/Portal/XF/Admin/Controller/Forum.php"
```php title="src/addons/Demo/Portal/XF/Admin/Controller/ForumController.php"
protected function saveTypeData(FormAction $form, \XF\Entity\Node $node, \XF\Entity\AbstractNode $data)
{
parent::saveTypeData($form, $node, $data);
Expand All @@ -403,7 +403,7 @@ This particular method's argument list assumes that we have a `use` declaration

So, right now, we've extended that method, and our extension should be called, but right now it isn't doing anything other than calling its parent method. We now need to get the value of the input from the forum edit page and apply that to the `$data` entity (which in this case is the Forum entity).

```php title="src/addons/Demo/Portal/XF/Admin/Controller/Forum.php"
```php title="src/addons/Demo/Portal/XF/Admin/Controller/ForumController.php"
protected function saveTypeData(FormAction $form, \XF\Entity\Node $node, \XF\Entity\AbstractNode $data)
{
parent::saveTypeData($form, $node, $data);
Expand All @@ -425,35 +425,35 @@ We've added a new column to the forum entity which will allow us to automaticall

In XenForo, we make heavy use of Service objects. These typically take a "setup and go" type approach; you setup your configuration and then call a method to complete the action. We use a service object to setup and perform the thread creation, so this is a perfect place to add the code we need. It all starts with another class extension, so go to the "Add class extension" page.

This time, the base class will be `XF\Service\Thread\Creator` and the extension class will be `Demo\Portal\XF\Service\Thread\Creator` and, as usual, this new class will look something like the code below. Create that code in the path `src/addons/Demo/Portal/XF/Service/Thread/Creator.php` then click "Save" to create the extension.
This time, the base class will be `XF\Service\Thread\CreatorService` and the extension class will be `Demo\Portal\XF\Service\Thread\CreatorService` and, as usual, this new class will look something like the code below. Create that code in the path `src/addons/Demo/Portal/XF/Service/Thread/CreatorService.php` then click "Save" to create the extension.

```php title="src/addons/Demo/Portal/XF/Service/Thread/Creator.php"
```php title="src/addons/Demo/Portal/XF/Service/Thread/CreatorService.php"
<?php

namespace Demo\Portal\XF\Service\Thread;

class Creator extends XFCP_Creator
class CreatorService extends XFCP_CreatorService
{

}
```

While we're here we will also create another extension. The base will be `XF\Pub\Controller\Forum` and the extension class will be `Demo\Portal\XF\Pub\Controller\Forum`. Creating the following code in the path `src/addons/Demo/Portal/XF/Pub/Controller/Forum.php` and click "Save":
While we're here we will also create another extension. The base will be `XF\Pub\Controller\ForumController` and the extension class will be `Demo\Portal\XF\Pub\Controller\ForumController`. Creating the following code in the path `src/addons/Demo/Portal/XF/Pub/Controller/ForumController.php` and click "Save":
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a comma after “here” for readability.
Small grammar fix: “While we're here, we will also create another extension.”

🧰 Tools
🪛 LanguageTool

[typographical] ~433-~433: There might be a comma missing.
Context: ...CP_CreatorService { } ``` While we're here we will also create another extension. ...

(IF_PRP_PRP_COMMA)

🤖 Prompt for AI Agents
In `@docs/lets-build-an-add-on.md` at line 433, Update the sentence in the
documentation text to include a comma after "here": change "While we're here we
will also create another extension." to "While we're here, we will also create
another extension." in the docs/lets-build-an-add-on.md file (the line
describing creating the extension class
Demo\Portal\XF\Pub\Controller\ForumController and the file path
src/addons/Demo/Portal/XF/Pub/Controller/ForumController.php).


```php title="src/addons/Demo/Portal/XF/Pub/Controller/Forum.php"
```php title="src/addons/Demo/Portal/XF/Pub/Controller/ForumController.php"
<?php

namespace Demo\Portal\XF\Pub\Controller;

class Forum extends XFCP_Forum
class ForumController extends XFCP_ForumController
{

}
```

We're ultimately going to extend the `_save()` method in our extended thread creator object so we can feature our thread after it has been created. To fit in with the "setup and go" approach, we will create a method which can be used to indicate whether the thread should be created as featured, or not. For this, we need two things; a class property to store the value (it defaults to null) and a public method to allow that property to be set.

```php title="src/addons/Demo/Portal/XF/Service/Thread/Creator.php"
```php title="src/addons/Demo/Portal/XF/Service/Thread/CreatorService.php"
protected $featureThread;

public function setFeatureThread($featureThread)
Expand All @@ -464,10 +464,10 @@ public function setFeatureThread($featureThread)

Going back to our newly extended forum controller, we will now extend the method that sets up the creator service, and opt in to featuring if the forum entity has the necessary value set. Remember, before extending a method, we need to know what it is expected to return (if anything), and ensure we call the parent method. If the parent method does return something, then it is this which we should return after our code has finished. The `setupThreadCreate()` method in this case returns the set up creator service, so we will start this off as follows:

```php title="src/addons/Demo/Portal/XF/Pub/Controller/Forum.php"
```php title="src/addons/Demo/Portal/XF/Pub/Controller/ForumController.php"
protected function setupThreadCreate(\XF\Entity\Forum $forum)
{
/** @var \Demo\Portal\XF\Service\Thread\Creator $creator */
/** @var \Demo\Portal\XF\Service\Thread\CreatorService $creator */
$creator = parent::setupThreadCreate($forum);

return $creator;
Expand All @@ -478,7 +478,7 @@ As expected, this doesn't actually do anything; the extended code is called, but

In between the `$creator` line and the `return` line, add:

```php title="src/addons/Demo/Portal/XF/Pub/Controller/Forum.php"
```php title="src/addons/Demo/Portal/XF/Pub/Controller/ForumController.php"
if ($forum->demo_portal_auto_feature)
{
$creator->setFeatureThread(true);
Expand All @@ -487,7 +487,7 @@ if ($forum->demo_portal_auto_feature)

We can now add the `_save()` method to the extended creator class:

```php title="src/addons/Demo/Portal/XF/Service/Thread/Creator.php"
```php title="src/addons/Demo/Portal/XF/Service/Thread/CreatorService.php"
protected function _save()
{
$thread = parent::_save();
Expand All @@ -498,7 +498,7 @@ protected function _save()

To make sure this thread gets featured, in between the `$thread` line and the `return` line we just need to add:

```php title="src/addons/Demo/Portal/XF/Service/Thread/Creator.php"
```php title="src/addons/Demo/Portal/XF/Service/Thread/CreatorService.php"
if ($this->featureThread && $thread->discussion_state == 'visible')
{
/** @var \Demo\Portal\Entity\FeaturedThread $featuredThread */
Expand Down Expand Up @@ -599,8 +599,9 @@ This doesn't return the results of this query. This returns the finder object it
Let's now use that in our `actionIndex()` method inside our portal controller. Change the existing `$viewParams = [];` line to the following:

```php title="src/addons/Demo/Portal/Pub/Controller/Portal.php"
/** @var \Demo\Portal\Repository\FeaturedThread $repo */
$repo = $this->repository('Demo\Portal:FeaturedThread');
use Demo\Portal\Repository\FeaturedThread;

$repo = $this->repository(FeaturedThread::class);

$finder = $repo->findFeaturedThreadsForPortalView();

Expand Down Expand Up @@ -705,7 +706,7 @@ public static function homePageUrl(&$homePageUrl, \XF\Mvc\Router $router)
}
```

Finally, we should consider changing the index page route to our portal page. Go to Admin CP and under Setup click Options followed by "Basic board information". Change the "Index page route" option to `portal/`.
Finally, we should consider changing the index page route to our portal page. Go to Admin CP and under Setup click Options followed by "Basic options". Change the "Index page route" option to `portal/`.

While you're in the Admin CP, let's see what happens now when you click on the Board title in the header. This should take you to your index page. All being well, that index page should now be your portal! In addition to that, the Home tab should be visible, and selected.

Expand Down Expand Up @@ -758,7 +759,7 @@ namespace Demo\Portal\XF\Entity;

class Thread extends XFCP_Thread
{
public function canFeatureUnfeature()
public function canFeatureUnfeature(&$error = null): bool
{
return true;
}
Expand All @@ -769,16 +770,16 @@ Ok, so, we haven't exactly done much here of value, yet. All the `canFeatureUnfe

To test this works so far, open one of the threads you previously featured, and select "Edit thread" from the tools menu. We should see the "Set thread status" checkbox row has the "Featured" checkbox we added, and it should be checked, indicating that this thread is indeed featured.

We can now move on to changing the thread editor service to look for this value and feature or unfeature accordingly. We are going to need two new class extensions for this. Go back to the "Add class extensions" page. The first one will have a base class of `XF\Pub\Controller\Thread` and extension class of `Demo\Portal\XF\Pub\Controller\Thread`. The second one will have a base class of `XF\Service\Thread\Editor` and an extension class of `Demo\Portal\XF\Service\Thread\Editor`.
We can now move on to changing the thread editor service to look for this value and feature or unfeature accordingly. We are going to need two new class extensions for this. Go back to the "Add class extensions" page. The first one will have a base class of `XF\Pub\Controller\ThreadController` and extension class of `Demo\Portal\XF\Pub\Controller\ThreadController`. The second one will have a base class of `XF\Service\Thread\EditorService` and an extension class of `Demo\Portal\XF\Service\Thread\EditorService`.

The editor service is actually going to be very similar to the extended creator service we created earlier, so create that in the relevant location. Here is all of the code for the extended class:

```php title="src/addons/Demo/Portal/XF/Service/Thread/Editor.php"
```php title="src/addons/Demo/Portal/XF/Service/Thread/EditorService.php"
<?php

namespace Demo\Portal\XF\Service\Thread;

class Editor extends XFCP_Editor
class EditorService extends XFCP_EditorService
{
protected $featureThread;

Expand Down Expand Up @@ -825,16 +826,16 @@ In the case of unfeaturing, we actually just delete the featured thread entity b

Before we finish the process of editing, we need to add code to our extended thread controller, and specifically extend the `setupThreadEdit()` method. The entire extended thread controller code will look like this:

```php title="src/addons/Demo/Portal/XF/Pub/Controller/Thread.php"
```php title="src/addons/Demo/Portal/XF/Pub/Controller/ThreadController.php"
<?php

namespace Demo\Portal\XF\Pub\Controller;

class Thread extends XFCP_Thread
class ThreadController extends XFCP_ThreadController
{
public function setupThreadEdit(\XF\Entity\Thread $thread)
{
/** @var \Demo\Portal\XF\Service\Thread\Editor $editor */
/** @var \Demo\Portal\XF\Service\Thread\EditorService $editor */
$editor = parent::setupThreadEdit($thread);

$canFeatureUnfeature = $thread->canFeatureUnfeature();
Expand All @@ -854,8 +855,8 @@ We need to extend another method in the thread controller to handle a situation

We just need to add the following code below the `setupThreadEdit()` method we added above:

```php title="src/addons/Demo/Portal/XF/Pub/Controller/Thread.php"
public function finalizeThreadReply(\XF\Service\Thread\Replier $replier)
```php title="src/addons/Demo/Portal/XF/Pub/Controller/ThreadController.php"
public function finalizeThreadReply(\XF\Service\Thread\ReplierService $replier)
{
parent::finalizeThreadReply($replier);

Expand All @@ -876,7 +877,7 @@ Note that we haven't actually returned anything in this method because it isn't

For the final step in manually featuring/unfeaturing a thread, we need to go back to the forum controller and slightly change our existing code so that if featuring isn't automatic, we can handle it manually, instead. This should be fairly straight forward. Head into your extended forum controller, and replace this:

```php title="src/addons/Demo/Portal/XF/Pub/Controller/Thread.php"
```php title="src/addons/Demo/Portal/XF/Pub/Controller/ForumController.php"
if ($forum->demo_portal_auto_feature)
{
$creator->setFeatureThread(true);
Expand All @@ -885,7 +886,7 @@ if ($forum->demo_portal_auto_feature)

With this:

```php title="src/addons/Demo/Portal/XF/Pub/Controller/Thread.php"
```php title="src/addons/Demo/Portal/XF/Pub/Controller/ForumController.php"
if ($forum->demo_portal_auto_feature)
{
$creator->setFeatureThread(true);
Expand Down Expand Up @@ -1058,20 +1059,21 @@ You may have spotted earlier in the demo_portal_view template that each post we
'attachments': $post.attach_count ? $post.Attachments : [],
```

Right now, this is going to generate an additional query for each post. So, we should instead try to do a single query for all of the posts we are displaying and add them to the posts in advance. It probably sounds more complicated than it is. Just add the below code beneath the `->slice(0, $perPage, true);` line.
Right now, this is going to generate an additional query for each post. So, we should instead try to do a single query for all of the posts we are displaying and add them to the posts in advance. It probably sounds more complicated than it is. Just add the below code beneath the `->sliceToPage($page, $perPage);` line.

```php title="src/addons/Demo/Portal/Pub/Controller/Portal.php"
use XF\Repository\AttachmentRepository;

$threads = $featuredThreads->pluckNamed('Thread');
$posts = $threads->pluckNamed('FirstPost', 'first_post_id');

/** @var \XF\Repository\Attachment $attachRepo */
$attachRepo = $this->repository('XF:Attachment');
$attachRepo = $this->repository(AttachmentRepository::class);
$attachRepo->addAttachmentsToContent($posts, 'post');
```

We use the `pluckNamed()` method first to get a collection of threads, then again to get a collection of the posts (keyed by the post ID) from the threads. Once we have the posts, we can just pass them into a special method in the attachment repo, which performs a single query and "hydrates" the Attachments relation for each post.

The final permission related thing to finish up is to create a new permission to control who can feature / unfeature threads manually. To do this, in the Admin CP under "Development" click "Permission definitions" and click "Add permission". The "Permission group" will be "forum", "Permission ID" will be `demoPortalFeature`, "Title" should be `Can feature / unfeature threads`, set "Interface group" to `Forum moderator permissions` and after choosing an appropriate display order and ensuring your add-on is selected, click "Save".
The final permission related thing to finish up is to create a new permission to control who can feature / unfeature threads manually. To do this, in the Admin CP under "Development" click "Permission definitions" and click "Add permission". The "Permission group" will be `forum`, "Permission ID" will be `demoPortalFeature`, "Title" should be `Can feature / unfeature threads`, set "Interface group" to `Forum moderator permissions` and after choosing an appropriate display order and ensuring your add-on is selected, click "Save".

To actually use this permission, we need to go back to our extended thread entity to modify the `canFeatureUnfeature()` method. Replace `return true;` with:

Expand All @@ -1087,7 +1089,7 @@ We currently display only 5 featured threads per page, but it would be nice to h

In the Admin CP under Setup then Options click the "Add option group" button. We'll just call the "Group ID" `demoPortal` and give it a title of "Demo - Portal options". Give it an appropriate ̀"Description" and "Display order" and click "Save".

Now click "Add option". Set the "Option ID" to `demoPortalFeaturedPerPage`, "Title" to `Featured threads per page`, edit format to `Spin box`, "Data type" to `Positive integer` and "Default value" to `10`. Click "Save".
Now click "Add option". Set the "Option ID" to `demoPortalFeaturedPerPage`, "Title" to `Featured threads per page`, edit format to `Number box`, "Data type" to `Positive integer` and "Default value" to `10`. Click "Save".

To implement that, go back to the portal controller and change:

Expand Down
Binary file modified static/files/Demo-Portal-1.0.0 Alpha.zip
Binary file not shown.