Knickers: Change your underwear!
Overview

This document is meant as an introduction into writing Pages in Knickers. At this point it primarily deals with Pages from the web side of things.

Terminology

There are some vocabulary words you should know before we get into how things work:

Creating a basic page

(Note: These instructions assume you have made your common directory web-accessible)

To create a basic Page you will need to:

  1. Setup an executioner script. The most basic script will look like this:
    require_once '../cfg/knickers_app.cfg.php';
    
    // load up the base Executioner class and run!
    require_once KNICKERS_ROOT.'/common/cls/Controller/Executioner.class.php';
    $executioner = new Executioner();
    $executioner->go();
    
    If you want to skip the .php extension on your executioner, you can make it runnable using a .htaccess file.
    <Files [executioner script name]>
    ForceType application/x-httpd-php
    </Files>
    
  2. Create a Page class by extending the Page class in Knickers. You're going to want to name your page "Page_[my page].class.php" and put it in your Controller directory.

  3. Create a template for your page and put it in the tpl/html directory

  4. Set a member var in your Page class called $baseTemplate to the name of your template file.
    class Page_MyPage extends Page
    {
    	var $baseTemplate = 'my_page.tpl';
    }
    
  5. You can now access your page at http://[your domain]/[your executioner]/[your page without Page_]
    http://www.yourdomain.com/page/MyPage, for example
    

Setting a default page

Say you want people to be able to come to just to yourdomain.com/page (or you're going to redirect them to page) and end up on your page. To make this happen you can set a default page in your executioner script.

$executioner = new Executioner('Page_', 'MyPage');

Setting an outer page

It's very likely that you'll want all of your pages to share some basic appearance and behaviors. This might include things like css, javascript, a navbar, etc. To accomplish this, you're probably going to want to setup an outer page class for yourself.

class Page_MyOuterPage extends Page
{
	var $baseTemplate = 'outer.tpl';
}
Now you need to tell the executioner about this page:
$executioner->setOuterPageClass('Page_MyOuterPage');
This will have the effect of running your Page_MyOuterPage class every time someone comes to yourdomain.com/page/[something]. That's great, but how do you get your pages to show up?

To do this, you're probably going to want to add a Panel using the URL. Assuming you have a tag called "MAIN_CONTENT" in your outer template, it would look something like this:

class Page_MyOuterPage extends Page
{
	var $baseTemplate = 'outer.tpl';
	
	function init()
	{
		if($this->addPanelFromURL('MAIN_CONTENT') === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
	
		// don't forget this!  It will init the Page you've just added
		if(parent::init() === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
	}
}
Note that it's possible to have multiple executioners with different outer page classes. You may want to do this if you have different sections of your site meant to be accessed by different users:
http://www.yourdomain.com/admin/Home
vs
http://www.yourdomain.com/lowly_user/Home

Outer Pages and Default Pages

Unfortunately, once you've set an outer page class, the executioner is going to ignore the default page you've told it to use. To replicate this functionality from within your outer page, simply send your default panel class name to addPanelFromURL() as a second param.
class Page_MyOuterPage extends Page
{
	var $baseTemplate = 'outer.tpl';
	
	function init()
	{
		// get our panel from the URL, but fall back to the my page if not found
		if($this->addPanelFromURL('MAIN_CONTENT', 'Page_MyPage') === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
	
		// don't forget this!  It will init the Page you've just added
		if(parent::init() === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
	}
}

Including Panels within Panels

Okay, now you've got your pages rendering in a consistent outer template, but you also want to add a navbar that's in a separate template. How do you add a child panel yourself?

require_once KNICKERS_APP_ROOT.'/common/cls/Controller/Panel_MyNavbar.class.php';

class Page_MyOuterPage extends Page
{
	var $baseTemplate = 'outer.tpl';
	
	function init()
	{
		// get our panel from the URL, but fall back to the my page if not found
		if($this->addPanelFromURL('MAIN_CONTENT', 'Page_MyPage') === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
	
		// create your navbar object
		$navbar = new Panel_MyNavbar($this->cfg, $this->ecos, $this->lm, $this->params, 
						$this->og, $this->locale, $this->perm);
		
		// and add it to a tag called 'NAVBAR'
		if($this->addPanelForTag('NAVBAR', $navbar) === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
			
		// don't forget this!  It will init the Page and Panel you've just added
		if(parent::init() === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
	}
}
Now you've got two panels - your navbar and the page itself.

Page Titles

Okay, so now your page is showing up and you've got your navbar, but how do you get the page's title to parse into your outer template's Documentation tag?

You'll do this by setting a 'titleMessageKey' member variable to the key for a defined message in your dictionary:

class Page_MyPage extends Page
{
	var $baseTemplate = 'my_page.tpl';
	var $titleMessageKey = MSG_MY_PAGE_TITLE;
}
Then, your outer page needs to ask your page for this information (note that child panels may be accessed by $this->childPanels['TAG_NAME']):
require_once KNICKERS_APP_ROOT.'/common/cls/Controller/Panel_MyNavbar.class.php';

class Page_MyOuterPage extends Page
{
	var $baseTemplate = 'outer.tpl';
	
	function init()
	{
		// get our panel from the URL, but fall back to the my page if not found
		if($this->addPanelFromURL('MAIN_CONTENT', 'Page_MyPage') === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
	
		$this->og->setTag('PAGE_TITLE', $this->childPanels['MAIN_CONTENT']->getTitle());			
			
		// create your navbar object
		$navbar = new Panel_MyNavbar($this->cfg, $this->ecos, $this->lm, $this->params, 
							$this->og, $this->locale, $this->perm);

		
		// and add it to a tag called 'NAVBAR'
		if($this->addPanelForTag('NAVBAR', $navbar) === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
			
		// don't forget this!  It will init the Page and Panel you've just added
		if(parent::init() === CDMJ_ERROR)
			return $this->setCDMJError($this->getCDMJError());
	}
}

Parent Page Permissions

Okay, that's great, but it seems kind of open, doesn't it? What if you don't want just any panel being added as a child of any other panel? Maybe you don't want your navbar getting tossed onto a page's template. How do you prevent that?

You're going to want to setup a member array in your child panel that explicitly lists which parent page/panel classes are allowed to run it:

class Panel_MyNavbar extends Panel
{
	var $allowedParents = array('Page_MyOuterPage');
	var $baseTemplate = 'navbar.tpl';
}
(Note that the parent check is done with is_a, so you may make a child panel runnable by any page/panel class that extends a certain base class simply by listing the parent class.)

This will have the effect checking permissions only on those child page/panels that specify their allowed parents. But what if you want to enforce listing these parents?

You can do this by setting a constant in your knickers_app.cfg file:

define('CHILD_PANEL_RUNNABLE_BY_DEFAULT', FALSE);

More specific Permissions

Okay, so now you've got your navbar only showing in the outer template, but what if you want it to be only visible to users with certain permissions and give an error otherwise? You might do something like this:

class Panel_MyNavbar extends Panel
{
	var $allowedParents = array('Page_MyOuterPage');
	var $baseTemplate = 'navbar.tpl';
	
	function isAvailable()
	{
		return $this->perm->canSeeNavbar();
	}
}
This will cause your entire page to error out if anyone attempts to view the navbar without the proper permissions. However, the error message provided my not be to your liking. To override it, setup a message for your error message and do:
class Panel_MyNavbar extends Panel
{
	var $allowedParents = array('Page_MyOuterPage');
	var $baseTemplate = 'navbar.tpl';
	
	function isAvailable()
	{
		if(!$this->perm->canSeeNavbar())
		{
			$this->setNotAvailableMessage(new Message('MSG_NO_NAVBAR'));
			return FALSE;
		}
		
		return TRUE;
	}
}
But what if you want to do more than just show a certain message? What if you want to, say, redirect your user to another page? For this you'll want the runWhenNotAvailable function, which, as its name implies, will run when your panel is not available:
class Panel_MyNavbar extends Panel
{
	var $allowedParents = array('Page_MyOuterPage');
	var $baseTemplate = 'navbar.tpl';
	
	function isAvailable()
	{
		if(!$this->perm->canSeeNavbar())
		{
			$this->setNotAvailableMessage(new Message('MSG_NO_NAVBAR'));
			return FALSE;
		}
		
		return TRUE;
	}
	
	function runWhenNotAvailable()
	{
		header('Location: [somewhere]');
	}
}
(Note that the default behavior is to show a permissions error and not run if any child panel is not available.)

Making your page do something

The default behavior of a page is nothing more than setting up its template and parsing it. To get it to do something, you're going to need to implement its run function:

class Page_MyOuterPage extends Page
{
	var $baseTemplate = 'outer.tpl';
	
	function init()
	{
		// get our panel from the URL, but fall back to the my page if not found
		if($this->addPanelFromURL('MAIN_CONTENT', 'Page_MyPage') === CDMJ_ERROR)
			return $this->setCDMJError(new CDMJ_ERROR($this->getCDMJError()));
	
		$this->og->setTag('PAGE_TITLE', $this->childPanels['MAIN_CONTENT']->getTitle());			
			
		// create your navbar object
		$navbar = new Panel_MyNavbar($this->cfg, $this->ecos, $this->lm, $this->params, 
							$this->og, $this->locale, $this->perm);
		
		// and add it to a tag called 'NAVBAR'
		if($this->addPanelForTag('NAVBAR', $navbar) === CDMJ_ERROR)
			return $this->setCDMJError(new CDMJ_ERROR($this->getCDMJError());
			
		// don't forget this!  It will init the Page and Panel you've just added
		if(parent::init() === CDMJ_ERROR)
			return $this->setCDMJError(new CDMJ_ERROR($this->getCDMJError()));
	}
	
	function run()
	{
		// this just sets the date into our outer template
		$this->og->setTag('TODAYS_DATE', date('m/d/Y');
	
		// don't forget this!  It will run your child panels
		if(parent::run() === CDMJ_ERROR)
			return $this->setCDMJError(new CDMJ_ERROR($this->getCDMJError()));
	}
}
It's very important that you not forget to run the parent::run function when you have child panels. This is what causes them to run and be parsed.

Changing your template dynamically

Now that you've got some pages that do some stuff, you may find that what you want your page to display differs dramatically depending on what happens in your run function. How do you completely swap out your template?

To do this, first forgo the $baseTemplate member variable. You will have no base template added and your $this->og object will be the one from your parent Page (or the Executioner, if there is no parent Page). You can then add templates yourself but you will have to make them printable to get them to automatically display:

class Page_MyCrazyPage extends Page
{
	function run()
	{
		if([something])
		{
			if(($pageTpl =& $this->og->addTemplate('crazy_true.tpl')) === CDMJ_ERROR)
			  return $this->setCDMJError(new CDMJ_ERROR($this->og->getCDMJError()));
		}
		else
		{
			if(($pageTpl =& $this->og->addTemplate('crazy_false.tpl')) === CDMJ_ERROR)
			  return $this->setCDMJError(new CDMJ_ERROR($this->og->getCDMJError()));
		}
		
		// make whichever template we picked show up automatically
		$this->addPrintableOutputGenerator($pageTPL);
	}
}

Fetch

Finally, you are probably going to want to include some css or js in your page (many of the Knickers Components require common.js so you're definitely going to want to include that) but you may not want to make all of your css and js directories web-accessible. To make this happen, you'll want to use the fetch script. There is a version in the Knickers/common directory but you will probably want to create a version for your app as well:

require_once '../cfg/knickers_app.cfg.php';

// make sure we use the right config object (you can change this to your app's 
// config if you have one
require_once KNICKERS_ROOT.'/common/cls/Config.class.php';

require_once KNICKERS_ROOT.'/common/fetch';

Then, make this runnable using the same .htaccess trick as above:
<Files fetch>
ForceType application/x-httpd-php
</Files>
You may then load up css and js files by using the URL:
http://www.yourdomain.com/fetch/[your file]
(Note: This does the same custom/common/knickers search that happens when Config tries to find a class or template file, meaning that you can override css or javascript at the custom level if you wish. -- This isn't true but it should be!!)