• This forum has been archived. New threads and replies may not be made. All add-ons/resources that are active should be migrated to the Resource Manager. See this thread for more information.

[part 4] Creating a add-on to insert tabs in profile page (using hooks)

Status
Not open for further replies.

Fuhrmann

Well-known member
Creating a add-on to insert tabs in profile page (using hooks) (PART 1)
Creating a add-on to insert tabs in profile page (using hooks) (PART 2)
Creating a add-on to insert tabs in profile page (using hooks) (PART 3)

Hey guys! DaveL just give me some tips about the next tutorial:

Also, I dont know if you planned to create any more tutorials, but another interesting one based on the profile tab with the permissions you went through would be a "member notes" kind of add on.
Usergroups with permissions (admins/mods) can only see the tab on users profiles. Each tab has a text input box similar to that of the "profile posts" tab. Admins/Mods could then post a note which would only show to other Admins/Mods underneath the input box.

So, let's do it!

So, in this tutorial we gonna change the content of our tab . No more "user's like". Our tab will have now a input text, which only admin/moderators can see, and write down some notes about the user. We'll call our tab "Notes".

Step 1 - Where to save the content?

Let's see: We now will have a tab that hold some notes about some user. Looking this way, we know we'll have some place to save this notes. But where?

Yes, in database. You can define the fields and which way you prefer to create it, but we will following this:

Table: xf_user_notes
Fields:
note_id -> Auto-increment field, this is the id of every note;INT (10)
given_user_id -> The user who is giving the note;INT (10)
received_user_id -> The user who is receiving the note;INT (10)
note_message-> The message of the note;VARCHAR (255)
note_date-> When the note was given to the received_user_id;INT(10)

Now we know how our new database will look like, we need to create it.

Step 2 - Explaining the Installer
Knowing that now we'll use a database, we need to a installer. So what this mean? Mean that the installer will create our database every time someone install our addon.
- User download our add-on
- User install our add-on
- Installer create all the databases our add-on needs
- User start to use the add-on.
- User want to uninstall our add-on
- Installer has a function called "uninstall" so the created tables will drop since the user dont need them anymore.
- Fin.
See? The installer has two basics methods: Install and Uninstall.
Inside the Installer we will declare all database structure we want to create. Let's do it.

Step 3 - Creating the installer

This is what we have so far in our directories structure:
- newProfileTabs
---- Extend
------- ControllerPublic
----------- Member.php
---- Model
------- newProfileModel.php
---- Listener.php

Go to the folder of our add-on "newProfileTabs" and create a new file. Name it as "Installer.php". So, this is what we'll have:

- newProfileTabs
---- Extend
------- ControllerPublic
----------- Member.php
---- Model
------- newProfileModel.php
---- Installer.php (our new file!)
---- Listener.php

"What's next? How i suposed to know what i have to write in this file?"

You may ask. Well, remember in the last tutorial (PART 3) we looking inside the files of XenForo? So, lets do the same! Since XenForo has installed in your computer (or website) we know that XenForo have a installer too! What about we take a look at it, so we can grab some code and make our own installer?

Looking inside the folder library/Xenforo we see a folder:

Install

Open it. We know got more and more folder and files. Each folder: more files. Well, the best way is looking to one by one file and see if we got what we want, some file which has declarations of database structure...

..... (looking inside the files)......

WAIT! GOTCHA! We found! I found it, i found it!

Open the Data folder, it is in library/XenForo/Install/Data. Check the file MySql.php:

PHP:
$tables = array();

$tables['xf_addon'] = "
    CREATE TABLE xf_addon (
        addon_id VARCHAR(25) NOT NULL,
        title VARCHAR(75) NOT NULL,
        version_string VARCHAR(30) NOT NULL DEFAULT '',
        version_id INT UNSIGNED NOT NULL DEFAULT 0,
        url VARCHAR(100) NOT NULL,
        install_callback_class VARCHAR(75) NOT NULL DEFAULT '',
        install_callback_method VARCHAR(75) NOT NULL DEFAULT '',
        uninstall_callback_class VARCHAR(75) NOT NULL DEFAULT '',
        uninstall_callback_method VARCHAR(75) NOT NULL DEFAULT '',
        active TINYINT UNSIGNED NOT NULL,
        PRIMARY KEY (addon_id),
        KEY title (title)
    ) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci
";

I think we got luck, eh?
Now we have a example code, we can make our code. Open the Installer.php. First, as always we need to put a name in our file class. Let's do it:

PHP:
// folder/file
// Following the directories structure
class newProfileTabs_Installer
{

}

Now, the query for the installer create our table. Since we got a example, that's not to dificult to make our own query:

PHP:
protected static $table = array(
        'createQuery' => 'CREATE TABLE IF NOT EXISTS `xf_user_notes` (
            `note_id` int(10) NOT NULL AUTO_INCREMENT,
            `given_user_id` int(10) NOT NULL,
            `received_user_id` int(10) NOT NULL,
            `note_message` VARCHAR(255) NOT NULL,
            `note_date` int(11) NOT NULL,
              PRIMARY KEY (`note_id`)
        ) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;',
        'dropQuery' => 'DROP TABLE IF EXISTS `xf_user_notes`'
    );

Note that we have a array $table. Inside we have 'createQuery' and 'dropQuery'. We gonna execute each one for the install() and uninstall() methods respectively.

So, let's create our install method:

PHP:
//Create the database
public static function install() {
    //We get here the instance of the XenForo db
    $db = XenForo_Application::get('db');

    //Tell the db to query our 'createQuery', remember?
    $db->query(self::$table['createQuery']);
}

That's it. We have our install function. Easy eh? How do you guess that we be our uninstall function? That's pretty more easy and almost the same:

PHP:
//Drop the database
public static function uninstall() {
    //We get here the instance of the XenForo db
    $db = XenForo_Application::get('db');

    //Tell the db to query our 'dropQuery', because we are uninstalling.
    $db->query(self::$table['dropQuery']);
}

See? We just changed the 'createQuery' to 'dropQuery'!

Our installer is finished:

PHP:
<?php// folder/file
// Following the directories structure
class newProfileTabs_Installer
{

    protected static $table = array(
        'createQuery' => 'CREATE TABLE IF NOT EXISTS `xf_user_notes` (
            `note_id` int(10) NOT NULL AUTO_INCREMENT,
            `given_user_id` int(10) NOT NULL,
            `received_user_id` int(10) NOT NULL,
            `note_message` VARCHAR(255) NOT NULL,
            `note_date` int(11) NOT NULL,
              PRIMARY KEY (`note_id`)
        ) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;',
        'dropQuery' => 'DROP TABLE IF EXISTS `xf_user_notes`'
    );

    //Create the database
    public static function install()
    {
        //We get here the instance of the XenForo db
        $db = XenForo_Application::get('db');

        //Tell the db to query our 'createQuery', remember?
        $db->query(self::$table['createQuery']);
    }

    //Drop the database
    public static function uninstall()
    {
        //We get here the instance of the XenForo db
        $db = XenForo_Application::get('db');

        //Tell the db to query our 'dropQuery', because we are uinstalling.
        $db->query(self::$table['dropQuery']);
    }

}
?>
 
Step 4 - Put the installer to work

The installer is in there. But is not working yes, because we did not associate with any add-on. To do this, open your AdminCP>List Add-ons. Click in our add-on "New Profile Tabs". The part that matters to us is this:

1.webp

Installation Code:
-----> First field: In the first field, we need to put our installer name class. We used newProfileTabs_Installer.
-----> Second field: In the second field, we put the method name to install: "install"
Uninstallation Code:
-----> First field: In the first field, we need to put our installer name class. We used newProfileTabs_Installer.
-----> Second field: In the second field, we put the method name to uninstall: "uninstall"

Hit "Save Add-on. With this we associate our two functions with the install and uninstall callbacks for the add-on.

Step 5 - It's working?

Go to your AdminCP>List Add-ons again and next to our add-on exist a "Control" button. Click it. Will open a menu, with some options. Choose the option "Export" and save in some place you want. I will save in my "C:\" just for a easy access. The name is "addon-newProfileTabs.xml".

Why do this? Because we want to uinstall our add-on and install again, to see if the table we want will be created, so this way, we will not lose any template we made. After exported, go to the button "Controls" again and click "Uninstall".

Confirm.

We now are able to install again and see if the install() method works.

Go to your AdminCP>Install Add-On and select the .xml that we exported before. (mine's in "C:\addon-newProfileTabs.xml"). Click "Install Add-on" and wait. (no errors should appear).

Is all occurs well, go to PHPMyAdmin of your website or test site and see if the tables exists:

2.webp

Yes, it worked. I hope you can get to work in there too.

Step 6 - Defining our objectives

Now that we have our database created and the new installer working, lets develop the rest.

Before continue let's review what we want to do:

1. Rename our profile tab to "Notes". To make the correct thing, lets create a phrase to hold that.
2. Change our template newProfileTab_ourTab_content
3. Create a new template to put a input text field in our tab content and a "Send" button and to hold our data.
4. Let only Administrators and Moderators see the notes in the tab and the tab.
5. Create a DataWriter to manipulate data
6. Create a model to hold all rules and definitions to our action.
7. Create a Controller for this action.
8. Below the input text field, load all message notes that adminstrators/moderators given to the user.

So, let's do it!
 
Step 7 - Rename our profile tab to "Notes"

We have two templates, so long in the PART 1 we created them. Here they are again:

newProfileTab_ourTab -> the main tab. This is where the user click to see the tab.
newProfileTab_ourTab_content -> The content of the tab. When user click on the tab, see this template.

Open the newProfileTab_ourTab. You got this:

PHP:
<li><a href="{$requestPaths.requestUri}#ourTab">Our Tab</a></li>

In this step we'll change "Our Tab" to "Notes" because that's what the tab will do. Show notes. Before continue, lets talk about phrases. The right way to print phrases to yours templates it's creating them. Instead of write directly the phrase to the template, let's create a new phrase in the menu Phrases and after append it in the template.

So, for now, go to AdminCP>Appearance>Phrases. Click the button "Create New Phrase". It will open a new page and i suggest you to fill with this:

Title: This is the title of the phrase. You can put anything you want in here, but for the pattern design lets always start a phrase with the name of our add-on. Our final phrase is: "Notes". So, the title will be newProfileTabs_notes.
Phrase text: Here we put the phrase. "Notes".
Add-on: As always we have to select which add-on pertences this phrase. Select New Profile Tabs which is ours.
Cache this phrase globally: Dont need to check this. This is only required if you want to use this phrase outside the template (via code).

3.webp

Now, open the template newProfileTab_ourTab and let's append the new phrase to this. For print out phrases we use this code:

Code:
{xen:phrase TITLE_OF_THE_PHRASE}

This is the final template after changing the "Our Tab" to the new phrase:

PHP:
<li><a href="{$requestPaths.requestUri}#notes">{xen:phrase newProfileTabs_notes}</a></li>

Note that i change too the "#ourTab" to "#notes". You can open any profile page and see if the tabs has been renamed to "Notes".

Step 8 - Input text field and "Send" button

Open the content template of our tab. (newProfileTab_ourTab_content)
We got that:

PHP:
<xen:require css="search_results.css" />
<li id="ourTab" class="profileContent" data-loadUrl="{xen:link members/userslike, $user}">
    <xen:if is="{$users_likes}">
        <xen:foreach loop="$users_likes" value="$like">
        <ol>
            <li class="searchResult post primaryContent" data-author="{$bookmark.user.username}">
            <div class="listBlock main">
                <div class="titleText">
                    <h3 class="title"><a href="posts/{$like.post_id}/">{$user.username} liked the post of {$like.receive_username}:</a></h3>
                </div>
                    <blockquote class="snippet">
                        {xen:helper snippet, $like.message, 150}
                    </blockquote>
                    <div class="meta">
                        Post by: <xen:username user="{$like}" rich="true" />,
                        Liked in <xen:datetime time="{$liked_content.like_date}" />
                    </div>
            </div>
            </li>
        </ol>
        </xen:foreach>
    <xen:else />
        <p>This user not liked any content yet.</p>
    </xen:if>
</li>

Lets Change our template newProfileTab_ourTab_content to:

PHP:
<li id="notes" class="profileContent" data-loadUrl="{xen:link members/notes, $user}">
    {xen:phrase loading}...
    <noscript><a href="{xen:link members/notes, $user}">{xen:phrase view}</a></noscript>
</li>

1. In the tutorial part 3, i already teach you guys about the data-loadUrl. The {xen:link members/notes, $user} will create a link like this: index.php?members/fuhrmann.1/notes, this is where our content will be loaded.
1.
Code:
data-loadUrl="{xen:link members/notes, $user}"

Now lets create another template, which will contain the input text field, a send button and will hold our data. Go to your AdminCP>Appareance>Templates>Create New Template.

Title of the template: newProfileTab_ourTab_notes

The final code is:

PHP:
<xen:require css="search_results.css" />
<div id="notes">
    <form action="{xen:link members/sendnote, $user}" method="POST" name="NoteForm" id="NoteForm">
        <input type="text" value="" name="note_message" class="textCtrl" style="width:80%"/>
        <input type="submit" value="Send" accesskey="s" class="button primary">
        <input type="hidden" name="_xfToken" value="{$visitor.csrf_token_page}" />
    </form>
    <xen:foreach loop="$notes" value="$note">
        <li>
            <ol>
                <li class="searchResult post primaryContent">
                    <xen:avatar user="$note.user" size="s" class="icon" />
                    <div class="listBlock main">
                        <blockquote class="snippet">
                            {$note.note_message}
                        </blockquote>
                        <div class="meta">
                            Note by: <xen:username user="$note.user" rich="true" />,
                            in <xen:datetime time="{$note.note_date}" />
                        </div>
                    </div>
                </li>
            </ol>
        </li>
    </xen:foreach>
</div>

1. This is the where whe will send the data after we click in the "Send" button. The "{xen:link members/sendnote, $user}" create this link: "index.php?members/fuhrmann.1/sendnote. Which "sendnote" will be our action defined inside the controller. Dont worry if you are not understanding this right now.
1.
Code:
action="{xen:link members/sendnote, $user}"

2. Our input text field to hold the message.
2.
PHP:
<input type="text" value="" name="note_message" class="textCtrl" style="width:80%"/>

3. The send button.
3.
PHP:
<input type="submit" value="Send" accesskey="s" class="button primary">

4. This is a token. For security reasons.
4.
PHP:
<input type="hidden" name="_xfToken" value="{$visitor.csrf_token_page}" />

5. This part maybe get a little bit strange for you, but here we define our loop throug all the notes that we will load. This will get explained more later.
5.
Code:
<xen:foreach loop="$notes" value="$note">

This is the code that will print the data of all notes (using foreach). There is a avatar, the username and the note sent. Dont worry, i explain more later.

PHP:
<li>
            <ol>
                <li class="searchResult post primaryContent">
                    <xen:avatar user="$note.user" size="s" class="icon" />
                    <div class="listBlock main">
                        <blockquote class="snippet">
                            {$note.note_message}
                        </blockquote>
                        <div class="meta">
                            Note by: <xen:username user="$note.user" rich="true" />,
                            in <xen:datetime time="{$note.note_date}" />
                        </div>
                    </div>
                </li>
            </ol>
        </li>

Chosse in the "addon-on" option, our add-on "New Profile Tabs".

And if you go to any profile page and click in the "Notes" tab:

4.webp
 
This message. What is this? Well, this means that we still dont have any actions called "Notes" in the Member Controller.
When the tab is clicked, the data-loadUrl get all the content from the url index.php?members/fuhrmann.1/notes. But, we still does not have the /notes action to pass any data throug the template.

We will define it later.

Step 9 - Administrators and Moderators

In this step we'll limit the access to the "Notes" tab. Remember we had a tutorial explaining this?

This is the conditional we'll use:
Code:
<xen:if is="{$visitor.is_admin} OR {$visitor.is_moderator}">
</xen:if>

So, we have to change only one template. Open newProfileTab_ourTab and let's add the code to check if the user is admin or moderator:

PHP:
<xen:if is="{$visitor.is_admin} OR {$visitor.is_moderator}">
    <li><a href="{$requestPaths.requestUri}#notes">{xen:phrase newProfileTabs_notes}</a></li>
</xen:if>

You may ask: "But what about the template data hold the content of this tab?"
We dont need to limit access, because the content of this data only load when someone click it. If you go to the any profile page, but dont click the tab "Notes", inspect the page, you'll see this:

5.webp

You can logout and test if it works.
 
Step 10 - Create a DataWriter to manipulate data

A DataWriter in a basic explanation:

Shadab said:
As the name suggests, a datawriter is responsable for manipulating data. ie, it would handle insertion, updation and deletion of data from the database. One thing you'd notice is a datawriter handles one row at a time. So unlike your Model from which you can select a bunch of rows in one go; a datawriter can manipulate a single row of data, only. Using a datawriter to manipulate data has an added benefit of maintaining integrity of your data.

Why we want a DataWriter? Because we will be making insertions into the database, updating, deletes and that's why DataWriter is in there. We can use it.

You have to create one folder and one file to this DataWriter. This is how your main folder "newProfileTabs" will look like:

- newProfileTabs
---- DataWriter (new folder!)
------- Notes.php (new file!)
---- Extend
------- ControllerPublic
----------- Member.php
---- Model
------- newProfileModel.php
---- Installer.php
---- Listener.php

The basic usage of a DataWriter:

PHP:
$dw = XenForo_DataWriter::create('NAME_OF_DATA_WRITER_CLASS');
$dw->set('field', $newValue);
$dw->save();

After this, lets analise one single example file of a DataWriter in XenForo. To open it, go to library/XenForo/DataWriter and open Poll.php. In this folder you can see other bunch of datawriters. But let's use this just to look inside.

1. As you can see the DataWriter Poll have a class name that extends to a abstract class called XenForo_DataWriter.
1.
PHP:
class XenForo_DataWriter_PollResponse extends XenForo_DataWriter

2. As the name says this function get all the fields defined for our table and define some types. If you add some new fields to the table, you have to make sure you'll update this file.
2.
PHP:
protected function _getFields()

3. "Gets the actual existing data out of data that was passed in." So, when we use a DataWriter we can update the fields too, instead of just insert a new row. This function make sure that you can update a record that already exists. Grab the content for the existing record so we can update.
3.
PHP:
protected function _getExistingData($data)

4. If you want to update a row, you must have a update condition. In this example the conditional is "WHERE poll_id = ". In our case will be "WHERE note_id = ".
4.
PHP:
protected function _getUpdateCondition($tableName)

5. Jumping off some custom functions of the Poll DataWriter, we have this: The preSave function. This always occurs BEFORE any $dw->save() we do. You can use the preSave function to validate some parameters.
5.
PHP:
protected function _preSave()

6. This function always occurs AFTER we call the $dw->save() function. We can add extra data in other fields and other things.
6.
PHP:
protected function _postSave()

7. This function always occurs AFETER we call the $dw->delete() function. We can call other functions, delete data from others tables and other things.
7.
PHP:
protected function _postDelete()

After this simple explanation, i think we can make our own DataWriter.

So, open the file Notes.php we've created. Lets name the class:

PHP:
<?php
//Following the directorie structure, remember?
class newProfileTabs_DataWriter_Notes extends XenForo_DataWriter
{
}
?>

Now, the _getFields functions. so the DataWriter will know which fields our database have. You can open our Installer.php file and copy all the fields of database and apply they here, as i did below:

PHP:
/**
* Gets the fields that are defined for the table. See parent for explanation.
*
* @return array
*/
protected function _getFields() {
    return array(
        'xf_user_notes' => array(
            'note_id' => array('type' => 'uint', 'autoIncrement' => true),
            'given_user_id' => array('type' => 'uint', 'required' => true),
            'received_user_id' => array('type' => 'uint', 'required' => true),
            'note_message' => array('type' => 'string', 'required' => true),
            'note_date' => array('type' => 'uint', 'default' => XenForo_Application::$time)
        )
    );
}

The next function we'll make is [/i]_getExistingData($data)[/i], so when we want to update a certain row in database, the DataWriter do the job for us and grab the row we want to update so we can do it.

First makes a call to the _getExistingPrimaryKey(), which gets the primary key from either an array of data or just the scalar value. So, we pass a array with data, then the _getExistingPrimaryKey gets the primaryKey and put in the $id var, so we can use to get all the data of the existing row just with the value of the primary key.

PHP:
/**
* Gets the actual existing data out of data that was passed in. See parent for explanation.
*
* @param mixed
*
* @return array|false
*/
protected function _getExistingData($data)
{
    if (!$id = $this->_getExistingPrimaryKey($data, 'note_id')) {
        return false;
    }

    return array('xf_user_notes' => $this->_getProfileTabsModel()->getNoteById($id));
}

You may ask now: "What is this "_getProfileTabsModel()" and "getNoteById()" ??"

And i answer: We have not implemented this yet, because we are making the DataWriter, but when we got in the creating of the Model i will explaing more. For now, lets say the "_getProfileTabsModel" is a function present here, in DataWriter, but did'nt write this yet. It gets our Model that we will create in other step and use all functions of it. And the getNoteById(), which is a function of our future Model will fetch one unique row just with the ID of the note.
The next funcion is the _getUpdateCondition(). It gets a SQL condition to update the existing record. As i explained above. So it will be:

PHP:
/**
* Gets SQL condition to update the existing record.
*
* @return string
*/
protected function _getUpdateCondition($tableName)
{
    return 'note_id = ' . $this->_db->quote($this->getExisting('note_id'));
}

The next function is in DataWriter too, but it can be in any other file, just to help the call to any funcions inside our model. Remember that we already have a model? We made it in the part 3 of this tutorial. The name is newProfileTabs_Model_newProfileModel.

If you don't remember, see THIS.

PHP:
/**
* @return newProfileTabs_Model_newProfileModel
*/
protected function _getProfileTabsModel()
{
    return $this->getModelFromCache('newProfileTabs_Model_newProfileModel');
}

For the DataWriter, that's it. This is the final code:

PHP:
<?php
class newProfileTabs_DataWriter_Notes extends XenForo_DataWriter
{

    /**
    * Gets the fields that are defined for the table. See parent for explanation.
    *
    * @return array
    */
    protected function _getFields() {
        return array(
            'xf_user_notes' => array(
                'note_id' => array('type' => 'uint', 'autoIncrement' => true),
                'given_user_id' => array('type' => 'uint', 'required' => true),
                'received_user_id' => array('type' => 'uint', 'required' => true),
                'note_message' => array('type' => 'string', 'required' => true),
                'note_date' => array('type' => 'uint', 'default' => XenForo_Application::$time)
            )
        );
    }

    /**
    * Gets the actual existing data out of data that was passed in. See parent for explanation.
    *
    * @param mixed
    *
    * @return array|false
    */
    protected function _getExistingData($data)
    {
        if (!$id = $this->_getExistingPrimaryKey($data, 'note_id')) {
            return false;
        }

        return array('xf_user_notes' => $this->_getProfileTabsModel()->getNoteById($id));
    }

    /**
    * Gets SQL condition to update the existing record.
    *
    * @return string
    */
    protected function _getUpdateCondition($tableName)
    {
        return 'note_id = ' . $this->_db->quote($this->getExisting('note_id'));
    }

    /**
    * @return newProfileTabs_Model_newProfileModel
    */
    protected function _getProfileTabsModel()
    {
        return $this->getModelFromCache('newProfileTabs_Model_newProfileModel');
    }
}
?>

Save it.

We still can't test anything now, but will be a pleasure when we can. Everything will work.
 
Step 11 - Creating the Model

As explained in the PART 3 of this tutorial:

Fuhrmann said:
Model: In this layer are defined rules of access and manipulation of data, which often stored in databases or files, but nothing indicates that serve accommodation only for persistent data. Can be used for data in volatile memory, eg RAM, although such use does not happen very often. All rules related to treatment, obtaining and validating data must be implemented in this layer.

In this step we will create our Model.

But if i remember, we already have one, right? We created it in the PART 3 of this tutorial. Here we go:

- newProfileTabs
---- DataWriter
------- Notes.php
---- Extend
------- ControllerPublic
----------- Member.php
---- Model
------- newProfileModel.php (hey! It's our model!)
---- Installer.php
---- Listener.php

Yes we already have a Model! Let's open it?

You can notice that inside of our Model we still have the function of the third tutorial: getAllGivenLikesByUser. Since we do not be using this anymore, delete it. Your Model will be like this:

PHP:
<?php
class newProfileTabs_Model_newProfileModel extends XenForo_Model
{
}
?>

In Step 10 (creating a DataWriter) we call a function called getNoteById() but we still dont have declared it in our Model, see:

PHP:
return array('xf_user_notes' => $this->_getProfileTabsModel()->getNoteById($id));

Now, its time to create it. With the newProfileModel.php Model opened lets add our first new function:

PHP:
/**
* Get a note by ID
* @param integer $noteId
*/
public function getNoteById ($noteId)
{
    return $this->_getDb()->fetchOne('
        SELECT *
            FROM `xf_user_notes` AS notes
        WHERE note_id = ?
        ', $noteId);
}

1. Gets the XenForo database object.
1.
PHP:
$this->_getDb()

2. We just want one row.
2.
PHP:
fetchOne

3. The SQl conditional says for itself: select all fields from our database, but only select the note who has the same ID we are passing into the function and return the row.

Next function!

Well, remember in Step 6 (when we defined our objectives):

"load all message notes that adminstrators/moderators given to the user."

We still have to create a query to get all notes of the user which we are seeing the profile. This functions could be called: getAllNotesReceivedByUserId(). Lets create then:

PHP:
/**
* Get all the notes that was received by a specific user
* @param integer $userId
*/
public function getAllNotesReceivedByUserId ($userId)
{
    return $this->_getDb()->fetchAll('
        SELECT
            notes.*,
            user.user_id, user.username, user.gender, user.avatar_date, user.gravatar
            FROM xf_user_notes AS notes
            INNER JOIN xf_user as user ON (user.user_id = notes.given_user_id)
        WHERE received_user_id = ? ORDER BY notes.note_date DESC
        ', $userId);
}

1. This time we want to fecht all the rows, not just one. So, lets use "fetchAll"
1.
Code:
fetchAll

2. With this query, we are selecting to the username, gender, avatar_date, gravatar to the user who GIVEN the note. Lets say:
Fuhr had wrote a note about Fuhrmann.

With this query lets take the note and all fields of it, the username, the gender, the avatar_date and the gravatar of Fuhr, so we can display a avatar later, in the result page.

Next function: prepareNotes(). Since we are selecting some fields from the xf_user table, lets put all those in a array, then we can call with something like that: $notes.user.username. This functions doest that:

PHP:
/**
* Prepare the array user in the notes
*/
public function prepareNotes(array $notes)
{
    foreach ($notes as &$note){
        $note['user'] = XenForo_Application::arrayFilterKeys($note, array(
                'user_id',
                'username',
                'gender',
                'gravatar',
                'avatar_date',
        ));
        unset($note['user_id'], $note['username'], $note['gender'], $note['gravatar'], $note['avatar_date']);
    }
    return $notes;
}

1. Here we unset this keys in array, since we already put them in $note['user'].
1.
PHP:
unset($note['user_id'], $note['username'], $note['gender'], $note['gravatar'], $note['avatar_date']);

If you want to know more about arrayFilterKeys go to library/XenForo/Application.php.

This is the final code:

PHP:
<?php
class newProfileTabs_Model_newProfileModel extends XenForo_Model
{

    /**
    * Get a note by ID
    * @param integer $noteId
    */
    public function getNoteById ($noteId)
    {
        return $this->_getDb()->fetchOne('
            SELECT *
                FROM `xf_user_notes` AS notes
            WHERE note_id = ?
            ', $noteId);
    }

    /**
    * Get all the notes that was received by a specific user
    * @param integer $userId
    */
    public function getAllNotesReceivedByUserId ($userId)
    {
        return $this->_getDb()->fetchAll('
            SELECT
                notes.*,
                user.user_id, user.username, user.gender, user.avatar_date, user.gravatar
                FROM xf_user_notes AS notes
                INNER JOIN xf_user as user ON (user.user_id = notes.given_user_id)
            WHERE received_user_id = ? ORDER BY notes.note_date DESC
            ', $userId);
    }

    /**
      * Prepare the array user in the notes
      */
    public function prepareNotes(array $notes)
    {
        foreach ($notes as &$note){
            $note['user'] = XenForo_Application::arrayFilterKeys($note, array(
                    'user_id',
                    'username',
                    'gender',
                    'gravatar',
                    'avatar_date',
            ));
            unset($note['user_id'], $note['username'], $note['gender'], $note['gravatar'], $note['avatar_date']);
        }
        return $notes;
    }
}
?>
 
Step 12 - Creating the Controller

In the past tutorial we have wrote a extended Controller with the name of Member.php, remember? See THIS.

So, lets find it:

- newProfileTabs
---- DataWriter
------- Notes.php
---- Extend
------- ControllerPublic
----------- Member.php (here i am! The extended Controller!)
---- Model
------- newProfileModel.php
---- Installer.php
---- Listener.php

We want to use the same controller because the URL of our "Notes" will be: index.php?members/fuhrmann.1/notes but we will not access direcly. Only throug the tab.

Open it. You should seeing now a action called actionUsersLike(). You dont need it anymore. Lets delete and this is what we will get after delete:

PHP:
<?php
class newProfileTabs_Extend_ControllerPublic_Member extends XFCP_newProfileTabs_Extend_ControllerPublic_Member
{
}
?>

Remember that in DataWriter we used a function that help us to get a instance of our Model? The name was _getProfileTabsModel(); So lets use it here too. In the Controller. Your file should be like this:

PHP:
<?php
class newProfileTabs_Extend_ControllerPublic_Member extends XFCP_newProfileTabs_Extend_ControllerPublic_Member
{
    /**
    * @return newProfileTabs_Model_newProfileModel
    */
    protected function _getProfileTabsModel()
    {
        return $this->getModelFromCache('newProfileTabs_Model_newProfileModel');
    }
}
?>

Ok. We need now to create a action to do what we want. We want to load in the tab content of the profile, all the notes that admin/mod have wrote about some user. Lets call the acion "Notes" (index.php?members/fuhrmann.1/notes). So the action will be this:

PHP:
/**
* Action to load all the users notes given by admin/moderators.
*/
public function actionNotes()
{
}

So far, so good? Ok, lets move on. What we want to do? When a admin/mod open the tab "Notes" the input and the button send appears and below, all the notes that was given to the user.
We need to find a way to load all of this data and put in our template!

But, wait! We already got it! In the Model! I remember! The function is getAllNotesReceivedByUserId! Nice, one guys. So, the complete action is this:

PHP:
/**
  * Action to load all the users notes given by admin/moderators.
*/
public function actionNotes()
{
    //Get the model
    $notesModel = $this->_getProfileTabsModel();

    //Filtering the user ID in the url
    $userId = $this->_input->filterSingle('user_id', XenForo_Input::UINT);

    //Get more fields
    $user = $this->getHelper('UserProfile')->assertUserProfileValidAndViewable($userId);

    //get all notes that was given to this user with this id
    $notes = $notesModel->getAllNotesReceivedByUserId($userId);
    //Prepare the user array
    $notes = $notesModel->prepareNotes($notes);

    /*Returning all the values to our template newProfileTab_ourTab_content
      with the variables $notes, and $user.*/
    $viewParams = array(
        'notes' => $notes,
        'user' => $user,
    );

    return $this->responseView(
        'XenForo_ViewPublic_Base', 'newProfileTab_ourTab_notes',    $viewParams
    );
}

Explaining:

1. Here we get the object of our Model, with the function _getProfileTabsModel. This way we can use all the functions inside the Model, and that's what we want. This is just a little help to us.
1.
PHP:
$notesModel = $this->_getProfileTabsModel();

2. Lets get the user_id of the user profile page, because we want to query the database for all the notes of this user! How this works? like this:
We have this URL: index.php?members/fuhrmann.1/notes
So the number 1 is our user_id. More explanation of this, maybe in other tutorial, this is about rout/prefixes.
2.
PHP:
$userId = $this->_input->filterSingle('user_id', XenForo_Input::UINT);

3. Here! We're using it! Our function! Oh...thats so nice. (i hope this works). Remember this function, eh? We have declared it in the Model that we've created. Get all the notes that was given to a certain user (usre_id).
3.
PHP:
$notes = $notesModel->getAllNotesReceivedByUserId($userId);

4. This is our response. We are returning two variables. The $user and the $notes to the template newProfileTab_ourTab_content, which is the template we wanna show the content we are making a query.
4.
PHP:
return $this->responseView(
'XenForo_ViewPublic_Base', 'newProfileTab_ourTab_content',array('user' => $user, 'notes' => $notes)
);

"So, that's it!"

Almost! But you can try, to see if its working. Go to any profile page, click in the tab "Notes" and see the action working.
This is what we'll got:

6.webp

But, wait, we have more. We still want to do something more that we haven't yet declared a controller. The "send" action. We want to send the notice to the user, right? But how?

Oh, this is almost the same thing about the other controller. We can do this using a route/prefix, but that is not the way i will do.

Lets create another action inside our extended Member Controller:

PHP:
/**
* Action to send a note to the specified user.
*/
public function actionSendNote()
{
}

With this action we need to get:

note message (note_message)
note date (note_date)
user who is given the note (given_user_id)
user who is receiving the note (received_user_id)

But how? Think with me: In our template newProfileTab_ourTab_notes we put one input text field and one button to submit the data. So here we have the message and the button to make this action happen. The others fields we gonna filter in other way.

Here is the final code of the action SendNote, below explained:

PHP:
/**
      * Action to send a note to the specified user.
    */
    public function actionSendNote()
    {
        //Make sure we are using a method form POST not GET
        $this->_assertPostOnly();

        //Lets get the ID of the user who is given the note
        $visitor = XenForo_Visitor::getInstance()->toArray();

        //The user ID who has received the note
        $userId = $this->_input->filterSingle('user_id', XenForo_Input::UINT);

        //Get the message content
        $note_message = $this->_input->filterSingle('note_message', XenForo_Input::STRING);

        //The date of the note given
        $note_date = XenForo_Application::$time;

        //Instance the DataWriter
        $dw = XenForo_DataWriter::create('newProfileTabs_DataWriter_Notes');
        $dw->set('given_user_id', $visitor['user_id']);
        $dw->set('received_user_id', $userId);
        $dw->set('note_message', $note_message);
        $dw->set('note_date', $note_date);
        $dw->save();

        $note = array('user' => $visitor, 'note_message' => $note_message, 'note_date' => $note_date);

        $viewParams = array(
            'note' => $note
        );

        return $this->responseView('XenForo_ViewPublic_Base', 'newProfileTab_ourTab_newnote', $viewParams);
    }

Explaining:

1. This make sure that the access to this action is only via POST.
1.
PHP:
$this->_assertPostOnly();

2. Here we got the visitor. The user who is writing the note, a administrator ou moderator. We use "toArray" because "getInstance" returns a object. But we want a array.
2.
PHP:
$visitor = XenForo_Visitor::getInstance()->toArray();

3. Here we got the ID of the user. This is the user who is receiving the note. The user of the profile page.
3.
PHP:
$userId = $this->_input->filterSingle('user_id', XenForo_Input::UINT);

4. The note message. Remember, we got a input text field in our content template with the name of "note_message".
4.
PHP:
$note_message = $this->_input->filterSingle('note_message', XenForo_Input::STRING);

5. The date of the note. If you want know more about it, open library/XenForo/Application.php and search "$time".
5. $note_date = XenForo_Application::$time;

6. Well, well! We are using our DataWriter that we created!
6.
PHP:
$dw = XenForo_DataWriter::create('newProfileTabs_DataWriter_Notes');

7. Here we set each field with each value we get. Then, we save it.
7.
PHP:
$dw->set('given_user_id', $visitor['user_id']);
$dw->set('received_user_id', $userId);
$dw->set('note_message', $note_message);
$dw->set('note_date', $note_date);
$dw->save();

8. We fill a variable $viewParams with a array of all the data we used in this action. With this, we can send then to our template to use it as response.
8.
PHP:
$note = array('user' => $visitor, 'note_message' => $note_message, 'note_date' => $note_date);

$viewParams = array(
'note' => $note
);

9. This is the respose. We did it before in the actionNotes(). We are passing the parameters to the template 'newProfileTab_ourTab_newnote'. We still dont have created this template, but we will. Dont worry.
9.
PHP:
return $this->responseView('XenForo_ViewPublic_Base', 'newProfileTab_ourTab_newnote', $viewParams);

You can see the final code of the file Member.php here.
 
Step 13 - Testing

Ok! Time to test!

Go to any profile page.
Click in the "Notes" tab.
Write a note in the input text field.
Click send.
You will receive a page, without any info. Just with the navbar of xenforo and a title "XenForo", right?

7.webp

That's because the response for the action we did (sendnote) stil doest not have the template response created.

Now, go back to the profile.
Click in the tab Notes again.

See? It works. Our DataWriter have inserted into the database a new record:

8.webp

So, besides the DataWriter working, we have our Model working and still the Controller with the two actins. All working. (i said that would be a pleasure when all got working)
 
Step 14 - New template

We now have the Controller with our actions. In the acionSendNote we have a responseView with a template that does not exists yet. We need to create, because we want to do this:
- Administrator write the note
- Click Send
- The note is appended above other notes in the page

This template will be responsable to show the new note wrote by some user.

Go to your AdminCP>Appearance>Templates and click the button "Create New Template".

Template Name: Give the name newProfileTab_ourTab_newnote

Now the template will be something like this:

PHP:
<li>
<ol>
    <li class="searchResult post primaryContent">
        <xen:avatar user="$note.user" size="s" class="icon" />
        <div class="listBlock main">
            <blockquote class="snippet">
                {$note.note_message}
            </blockquote>
            <div class="meta">
                Note by: <xen:username user="$note.user" rich="true" />,
                in <xen:datetime time="{$note.note_date}" />
            </div>
        </div>
    </li>
</ol>
</li>

Add-on: Choose our add-on "New Profile Tabs".

Save it.

"What this template does?"

This simple shows the content of the note that we recently wrote.
 
Step 14 - Testing again

So, FINALLY! Let's see if the template show us our note?

Go to any profile page, make sure you are logged as a administrator ou moderator
Go to the tab "Notes".
Write a message.
Click "Send".

This is what we got, right?
9.webp

All working fine! :) (i hope)
This is a bit ugly, eh? Why we just dont make some more "jqueryistic"?
 
Step 15 - Applying jquery!

We can use jquery to load our message sent directly to the template that we are actually send the message.

I am not going to explain detailed the jquery actions line by line. If you want to know more about AJAX calls, jquery, see this.

So, go to to your root installation of XenForo. Open the js folder:

-- data
-- install
-- internal_data
-- js (itsme, itsme!)
-- library
-- styles

Inside the js folder, lets create a new folder and rename to the name of our add-on: "newProfileTabs"

Get into the new folder newProfileTabs and create a new file, called notes.js. Open your new file.

The basic code to make this work:

PHP:
/** @param {jQuery} $ jQuery Object */
!function($, window, document, _undefined)
{
}
(jQuery, this, document);

Ok, what we want to do?

We want to bind the "submit" action on our form.
Get the template response.
Append in the actual template.

Back there, in the Step 8, we create a new template and put a input type and a send button. And, we declared a form:

PHP:
<form action="{xen:link members/sendnote, $user}" method="POST" name="NoteForm" id="NoteForm">

We need to use it ID attribute so we can tell jquery what to look for the submit.

This is the second part:

PHP:
/** @param {jQuery} $ jQuery Object */
!function($, window, document, _undefined)
{
    XenForo.NoteForm = function($form)
    {
    };
    XenForo.register('#NoteForm', 'XenForo.NoteForm');
}
(jQuery, this, document);

1. You can put any name here. We will using the same as ID attribute of this form.
1.
PHP:
XenForo.NoteForm

2. We need to register so the function will be called. Our form have a ID attribute "#NoteForm". So we use it.
2.
PHP:
XenForo.register('#NoteForm', 'XenForo.NoteForm');

Next step: define the what we'll do:

PHP:
/** @param {jQuery} $ jQuery Object */
!function($, window, document, _undefined)
{
    XenForo.NoteForm = function($form)
    {
        $form.submit(function(e)
        {
            e.preventDefault();

            alert('test');

            return false;
        });
    };
    XenForo.register('#NoteForm', 'XenForo.NoteForm');
}
(jQuery, this, document);

This is just a test. But to make this work, we need to require this js in the template file.

Open the template newProfileTab_ourTab_notes and below this:
Code:
<xen:require css="search_results.css" />

Add this:

Code:
<xen:require js="js/newProfileTabs/notes.js" />

We are now requiring the JS that we created to make work into the form in template.

Go to any profile page. Click the "Notes" tab. Dont need to write anything, just click in the "Send" button.

10.webp

Ok, we got it working, now lets do the real thing. Open the notes.js file and lets write our action:

PHP:
$form.submit(function(e)
{
    e.preventDefault();
    serialized = $form.serializeArray();
    XenForo.ajax($form.attr('action'), serialized, function(ajaxData)
    {
        $(ajaxData.templateHtml).xfInsert('insertAfter', $form, 'xfSlideDown', XenForo.speed.slow);
    });
    return false;
});

1. Prevent the default action of the form being trigged
1.
PHP:
e.preventDefault();

2. Encode a set of form elements as an array of names and values.
2.
PHP:
serialized = $form.serializeArray();

3. Here we make a ajax request, to the attribute "action" of our form, send the serialized data. "ajaxData" is the data returned.
3.
PHP:
XenForo.ajax($form.attr('action'), serialized, function(ajaxData)

4. Here we:
Got our response template (ajaxData.templateHtml)
Insert in some place (xfInsert)
Where? After some object (insertAfter)
Which object? (our $form)
The effect? (xfSlideDown)
And set the speed of the effect. (XenForo.speed.slow)
4.
PHP:
$(ajaxData.templateHtml).xfInsert('insertAfter', $form, 'xfSlideDown', XenForo.speed.slow);

This is a more and less explanation.

The complete final code of the file notes.js:

PHP:
/** @param {jQuery} $ jQuery Object */
!function($, window, document, _undefined)
{
    XenForo.NoteForm = function($form)
    {
        $form.submit(function(e)
        {
            e.preventDefault();
            serialized = $form.serializeArray();
            XenForo.ajax($form.attr('action'), serialized, function(ajaxData)
            {
                //$(ajaxData.templateHtml).xfInsert('prependTo', $form);
                $(ajaxData.templateHtml).xfInsert('insertAfter', $form, 'xfSlideDown', XenForo.speed.slow);
            });
            return false;
        });

    };
    XenForo.register('#NoteForm', 'XenForo.NoteForm');
}
(jQuery, this, document);
 
Thank you so much Fuhrmann. I think you are inspiring a lot of people to start coding with your very helpful tutorials!

This should keep me busy over the next few days going through and digesting everything! Looking forward to starting later!

Thanks again. The effort you put into creating these tutorials is really appreciated :)
 
Hi Fuhrmann,

Having a little bit of trouble at step 12. This is the code ive placed into member.php

PHP:
<?php
/**
      * Action to send a note to the specified user.
    */
    public function actionSendNote()
    {
        //Make sure we are using a method form POST not GET
        $this->_assertPostOnly();

        //Lets get the ID of the user who is given the note
        $visitor = XenForo_Visitor::getInstance()->toArray();

        //The user ID who has received the note
        $userId = $this->_input->filterSingle('user_id', XenForo_Input::UINT);

        //Get the message content
        $note_message = $this->_input->filterSingle('note_message', XenForo_Input::STRING);

        //The date of the note given
        $note_date = XenForo_Application::$time;

        //Instance the DataWriter
        $dw = XenForo_DataWriter::create('newProfileTabs_DataWriter_Notes');
        $dw->set('given_user_id', $visitor['user_id']);
        $dw->set('received_user_id', $userId);
        $dw->set('note_message', $note_message);
        $dw->set('note_date', $note_date);
        $dw->save();

        $note = array('user' => $visitor, 'note_message' => $note_message, 'note_date' => $note_date);

        $viewParams = array(
            'note' => $note
        );

        return $this->responseView('XenForo_ViewPublic_Base', 'newProfileTab_ourTab_newnote', $viewParams);
    }
?>

However the forum then comes back with this error:

Parse error: syntax error, unexpected T_PUBLIC in C:\xampp\htdocs\community\library\newProfileTabs\Extend\ControllerPublic\Member.php on line 5
 
it seems that you're using another file and not the one from pref. how-to

Lets create another action inside our extended Member Controller:


PHP:
/**
* Action to send a note to the specified user.
*/
public function actionSendNote()
{
}
 
You have to make sure that the actionNotes and aciontSendNote is in the file Member.php
(newProfileTabs_Extend_ControllerPublic_Member)
 
Thats all ive got in members.php. I copied the section where you said it was the final code.
 
Status
Not open for further replies.
Top Bottom