Jump to content






Photo
* * * * * 13 votes

Hook creation walkthrough

Posted by Michael , 14 June 2011 · 6,717 views

hook development walkthrough
So I recently tossed out the idea of writing up how I create hooks, and several people said that would be a good idea to help them learn the hook creation process. Thus, I'd like to take the opportunity to write up the steps I went through to create my latest new hook for IP.Board 3.2: User Online Status in Topics.

First off, I try and use a very clean install of the version of IP.Board I'm using as a development platform. I install several such little boards on my laptop for development purposes, most are dedicated to single larger projects (like the applications I write) while I tend to keep one for just little hooks I might create. I used that general development board for this.

That board has IN_DEV enabled, but only on the Admin side. I do this by editing /admin/index.php and adding this line near the top:

define('IN_DEV', 1);
The advantage here is that I get all of the development goodness of IN_DEV while in the Admin CP, but I don't have to worry about the whole master skins stuff, which I've always felt was more trouble than it is worth. I'll show you later on how I deal with skins without mucking about with master skin files.

Now, on to the actual development of this hook. The first thing I needed to do was find out where I'll be hooking in to. In IP.Board 3.0 and 3.1, the Topic View was all in the topicViewTemplate, with stuff around the avatar itself in its own userInfoPane template. The user name where the online indicator used to live was not in the userInfoPane area, so I started by looking where in the topicViewTemplate for where the name was. This confused me a bit at first as I couldn't find where the member name was anymore. I did some poking around in the template and found that they have moved the individual posts out of this topicViewTemplate and into their own 'post' skin template. So that's where I turned next.

Looking in the post template, I found the following area where the user name code was showing up:

<if test="postMember:|:$post['author']['member_id']">
					<span class="author vcard">{parse template="userHoverCard" group="global" params="$post['author']"}</span>
				<else />
					{$post['author']['members_display_name']}
				</if>
This was perfect, as it was an if statement, which is one of the items you need to do a skin template hook (that, or a foreach statement). So this would be the area where the online indicator would go. For consistency's sake I fired up a 3.1 test board to see where exactly they placed the online indicator, and saw it was just inside the opening <if> tag in this code:

<if test="postMember:|:$post['author']['member_id']">
							<if test="postOnline:|:$post['author']['_online']">
								{parse replacement="user_online"}
							<else />
								{parse replacement="user_offline"}
							</if>  
								<span class="author vcard"><a class="url fn" href='{parse url="showuser={$post['author']['member_id']}" base="public" template="showuser" seotitle="{$post['author']['members_seo_name']}"}'>{$post['author']['members_display_name']}</a>{parse template="user_popup" group="global" params="$post['author']['member_id'], $post['author']['members_seo_name']"}</span>
						<else />
							{$post['author']['members_display_name']}
						</if>
So I now knew exactly where I needed to hook into and what my basic code for the hook would be, as I'd just be duplicating this 3.1 functionality.

The next step was to start creating the hook in the Manage Hooks page. I went out to that page, clicked the Create New Hook button and started working through the various pages there. Here's the first page, most of this is pretty self-explanatory:

Attached Image

A few things to keep in mind. I give my apps a unique key there that I try to use throughout, you'll see that in use later. The only thing there that might be confusing for some folks is that Update URL, that's something not a lot of authors use but I like to in order to let my users know when a hook has been updated.

The next tab is for requirements of the hook. As this is a 3.2-only hook, the requirements were pretty basic:

Attached Image

That just indicates that the hook requires IP.Board 3.2. I don't use the Global Caches tab (I'm not even really sure why one would need that), so I moved on to the Files tab. Out here is where I set up the hook itself. This is what usually gets most people confused, so here's a screenshot and then I'll talk about this in more detail.

Attached Image

The fields at the top are pretty easy, you just want to give a file name and class name. I use that same hook key as the basis for both of these, and just add a .php extension to the file name. After I fill out these fields, the next step I do is actually create the PHP file. I'll come right back to that part though, let me finish talking about this page.

There are several different types of hooks available, but this kind is probably the easiest. The end result of what we're going to do with this hook is find something in the skin and add to it, so that is a template hook. When you choose that, the dropdowns below pop up to let you choose the exact hook you'll be making. As we already determined above, we're hooking into the topic skin (skin_topic), in the 'post' skin template, and in the 'postMember' if statement. As we want the code we'll be adding to execute right after the if statement starts, we choose the appropriate choice from the last dropdown.

Now let me jump back to the actual file. In the forum's /hooks directory, I created a file with the name given on this page, and populated it with the very basic code for a template hook:

<?php

class onlineStatusInTopics
{
	protected $registry;
	
	public function __construct()
	{
		/* Make registry objects */
		$this->registry = ipsRegistry::instance();
	}
	
	public function getOutput()
	{
		/* Return */
		return;
	}
}
We already knew what our class name was from above, and the __construct() function is a standard PHP function that gets called automatically when this class is called. That function should always be used to set up your common registry objects you'll need. The getOutput() function is what IP.Board is going to call to determine what needs to be added to the page at the selected hook point. This is the function most commonly used, but it has drawbacks. It's good when you have a distinct thing you need to add once, but we've got to loop through several posts on the page and show this for each one.

Fortunately, there is another option. Inside a template hook class, you can also have a function called replaceOutput(). It takes two arguments, $output and $key. $output is the actual HTML output of the page, $key is the unique identifier of the hook point. As its name implies, replaceOutput is used to find content in the page and replace it with something else. Here is the full content of the function for this hook. It may look confusing at first, but nearly all of this code will be what you'd always use in a hook of this type:

public function replaceOutput( $output, $key )
	{
		/* Got some data? */
		if ( is_array( $this->registry->output->getTemplate('topic')->functionData['post'] ) && count( $this->registry->output->getTemplate('topic')->functionData['post'] ) )
		{
			/* Init some vars */
			$tag  = '<!--hook.' . $key . '-->';
			$last = 0;
			
			/* Loop through each template call */
			foreach ( $this->registry->output->getTemplate('topic')->functionData['post'] as $k => $v )
			{
				/* See if we can find this hook point */
				$pos = strpos( $output, $tag, $last );
				
				/* Found? */
				if ( $pos !== FALSE )
				{
					/* Start swapping it out */
					$string = $this->registry->output->getTemplate('topic')->hookUserOnline( $v['post']['author'] );
					$output = substr_replace( $output, $string . $tag, $pos, strlen( $tag ) ); 
					$last   = $pos + strlen( $tag . $string );
				}
			}
		}
		
		/* Return */
		return $output;
	}
So let me walk through the code and explain how it works. First off, in a template hook, you automatically have access to that functionData array. You'll note that it defines both the skin group (topic) and template (post) we're hooking in to. Each skin template you could hook in to would have this same object available to you, you just have to ensure you're referring to it. We start off by just ensuring we have this object, and wrap all of our code in that check. We next set up those $tag and $last variables to help us move through the $output code to find what we're replacing. We next loop through all of our functionData items (those hold the 'data variables' items for each post, by the way, that's why they are so handy here), find the point in the $output where we're replacing stuff, and perform a substr_replace to add our text.

I bet a lot of you are scratching your head at this point, but don't worry. Like I mentioned, a lot of this code is standard, and is just needed to find the hook point and add the text you need. The only line in this code you need to concern yourself with is this:

$string = $this->registry->output->getTemplate('topic')->hookUserOnline( $v['post']['author'] );
This is the magic part. What we're doing here is calling a skin template and assigning that resulting HTML to a variable called $string. We need to create this template, and that will be the next part I talk about, so I'll come back to this. As you can see, it is a template in the topic view, and is called hookUserOnline(). It has one variable that gets passed to it, $v['post']['author']. That is a variable that is an array of all the data about the person who made the post. You may well be asking yourself how I figured out that was the variable, and that is a perfectly reasonable question. When writing source code like this, and I want to know what the values of variables are, I always use this construction:

print "<textarea cols='50' rows='20'>";
print_r( $variable );
print "</textarea>";
This prints a nice little textarea on the page with the value of the variable I'm checking. Since we're inside a foreach loop of the functionData, where $k is the index of the array and $v is the value, I checked what $v had available to me. Here's what the printout of that looks like, by the way:

Attached Image

So it was just a matter of checking through that to find how I could get info about the author. $v is a multi-dimensional array with lots of info in it. The first level there has data about the post, and it again is a multi-dimensional array. Under post there is another array called author that holds all of the data about the author. Again, you can go back to that print_r method to spit these items out to you to help you narrow down where the data you need is located. I found that $v['post']['author'] had all of the data about the author through this method, and I'd need to pass that to my template to check if they were online or not.

Now that pretty much wraps up this PHP file. You should note that even when using the replaceOutput() function, you have to also include that getOutput() function. IP.Board will always require that to be in place for template hooks. So now that the PHP file is built, you go back to the Admin CP and save that hook. The next step that must be done in this case is to create that skin template. We likely would have done that already, but let's cover it now.

In the Look & Feel tab, go to the default skin and add the new template you need, and name it whatever you want. Obviously we said earlier that we're naming this one hookUserOnline, and it has a data variable of author data. I called my data variable like this:

$author=array()
So then in my template, I have an array called $author which we passed from the PHP code, and it contains all of the data about our author. The code for my template was lifted right from the 3.1 install:

<if test="postOnline:|:$author['_online']">
								{parse replacement="user_online"}
							<else />
								{parse replacement="user_offline"}
							</if>
Now, I tried this thinking it would work like magic, but it did not. They changed from using those replacements to a different look for this indicator, so I needed to change to that. Doing that, and a bit of code tweaking, I came up with this code instead:

<if test="postOnline:|:$author['_online']">
	<span class='ipsBadge ipsBadge_green'>{$this->lang->words['online_online']}</span>
<else />
	<span class='ipsBadge ipsBadge_grey'>{$this->lang->words['online_offline']}</span>
</if>
Then I saved the template. I noticed that these two language strings were not available in the topic view, so they needed to be added. I did that through the Manage Languages page, and simply placed them in the IP.Board -> public_topic word pack.

Now, since I don't use Master skins in the recommended sense, I needed to move this template into the master skin so that it would be added to my hook export. The way I do that is to fire up phpMyAdmin, browse the skin templates table, and sort them by template_id descending. You'll see that there are two entries for this skin template you just added: one that has template_set_id set to 0 and nothing in the template_content field, and another where template_set_id is 1 and has template_content. The first one of these (template_set_id 0) is the master copy of the template. Delete that by clicking the red X. Then edit the other template and change the following: template_set_id, template_removable, template_added_to, template_user_added & template_user_edited should all be set to 0, and template_master_key should be set to root. Save that, and now that new template you made is the master.

All that we need to do now is export the hook. Back on the Manage Hooks page, go to the hook, click the dropdown beside it, and click Export Hook. You need to use this UI to choose what all you added as part of this hook. In this case we added a couple language strings and a skin template, so use those wizards to find those object we added, and then hit the Export button at the bottom. This will let you save the fully created hook somewhere. I always like to reimport it at this point just to ensure I did everything correctly.

Now, believe it or not, this entire process for this particular hook took me no more than 20 minutes from start to finish. It took me several times longer than this to write this all up, though, and I'm sure I might have missed some steps or glossed over some things I take for granted. So please let me know if this was confusing, or helpful. I'd love to be able to improve upon this as a means of helping people write their own hooks. It's really quite easy once you get the hang of it, and they can do a lot of cool things.

  • mdx, Gabriel Torres, IPB Bob and 13 others like this



Great article Michael! I'm sure this will help a lot of hook developers out there: I had bad experiences in the past with IN_DEV and the master skins due to IPB bugs or incorrect sequences of actions, and always hesiteated to activate this and write new hooks like this, but instead started from existing hooks files and modified their XML content, which is often very complicated and painfull! So I think I will use many of the nice tricks that you mention above and I really appreciate the effort needed to write this!
Can you do a post on how to create an app?
Photo
Mr Washington
Jun 14 2011 05:02 PM
Like the guy above I have been looking on how I can start making hooks! Thanks for the great post!
amazing. Simply amazing.

Great time and detail for a beautiful tutorial.
Bravo!

I especially liked the template bit part, your solution of going to the database feels a lot better!

Can you do a post on how to create an app?

Those are obviously harder, because there's so many more options available. How about a relatively simple one, just to give a walkthrough on the basic structure of an app? I'm thinking maybe a simple app where you just set some text in the Admin CP and then display it on a single public page. Sound good?
Photo
Mr Washington
Jun 14 2011 10:58 PM

Those are obviously harder, because there's so many more options available. How about a relatively simple one, just to give a walkthrough on the basic structure of an app? I'm thinking maybe a simple app where you just set some text in the Admin CP and then display it on a single public page. Sound good?


IDK about him but I like it ;)

Those are obviously harder, because there's so many more options available. How about a relatively simple one, just to give a walkthrough on the basic structure of an app? I'm thinking maybe a simple app where you just set some text in the Admin CP and then display it on a single public page. Sound good?

A while ago either Collin or Mark released the Hello World App.. just a test app to show how to create apps. I just can't for the life of me figure out how to replicate all of it. Not to mention that app doesn't work stock. It's just the files, no installation file. Perhaps you could dissect that app for us and get it working?


Or your idea would work perfectly as well.. I just have so many ideas for apps running around in my head and can't figure out how to create them :(.

Thanks again for this entry.
Very nice. Thanks Michael.
When you started talking about "Now let me jump back to the actual file," you lost me. The gear was shifted to coder mode which went over my head. Even after reading it a few times.

When you started talking about "Now let me jump back to the actual file," you lost me. The gear was shifted to coder mode which went over my head. Even after reading it a few times.

I talked about that just a little bit above that section:

After I fill out these fields, the next step I do is actually create the PHP file. I'll come right back to that part though, let me finish talking about this page.

What I'm referring to here is the PHP source file that every hook requires. This will be a file you write and place in the forum's /hooks directory. So I have the Admin CP still open on that page I'm talking about with the hook's settings, I've not saved the hook yet, and I go to my code editing program to start writing the source file. Knowing this, does the text there and in the next couple of sentences start to make sense, or is it still confusing?
Since, I have repeated all your steps, as an excercise, one silly question that I always wanted to ask long time ago:
When you say: "I always like to reimport it at this point just to ensure I did everything correctly.", you do this by "Install new hook" and providing the xml file? Does this always update the exising hook (if a hook with the same hookKey is already installed?) Or is there some other way (button), I cannot see/find?
Yeah, re-importing the hook just like you install new ones will upgrade the hook if it's already installed, or if it's the same version as what's already installed, will essentially do nothing.
Is it possible to load a language pack in a hook like you do in an application?
Absolutely, you'd do it the same way. Just make sure you set up $this->lang in the constructor. Oftentimes, though, a hook will add its language bits right into a language pack that is already loaded on the page the hook shows up on, so that no new pack needs loaded.
I have set up $this->lang = $this->registry->lang();

Then I have $this->lang->loadLanguageFile( array( 'public_ignores' ), 'ignores' );

but I'm getting a call to object error.

EDIT: Fatal error: Call to undefined method ipsRegistry::lang() in /home/xternals/public_html/ipb32/hooks/ignores_prof_view.php on line 15
The constructor for $this->lang is:

$this->lang   	=  $this->registry->getClass('class_localization');
    • SidV likes this
I never would have guessed that :S... Thanks.
If I ever forget one of those, I always look in /admin/sources/base/ipsController.php, in the various makeRegistryShortcuts() functions. Those have the complete lists of these available common constructor lines.
Could you make one entry about the different types of hooks? (overloaders etc.)
    • IPB Bob likes this

Recent Comments

0 user(s) viewing

0 members, 0 guests, 0 anonymous users

Latest Visitors

  • Photo
    Joey Prettyman
    Today, 12:01 PM
  • Photo
    Magda Biegaj
    18 Jul 2014 - 20:41
  • Photo
    ICRTouch
    18 Jul 2014 - 08:38
  • Photo
    xplosivsupps
    17 Jul 2014 - 19:55
  • Photo
    Pyranicus
    17 Jul 2014 - 19:23