I was bored about an hour ago while I was checking back on Elkarte. So I took about 45 minutes to write out a stop-gap for themes (I would prefer a better solution in the long run) which makes changing subtemplates easier IMO.
<?php
/**
* A template comprised of subtemplates
* @todo include methods like javascript_escape()
*/
class Template
{
public $context;
public $settings;
public $scripurl;
public $txt;
public $user_info;
public $options;
// @todo a container should be passed in here containing the Theme class instead of having Theme as static
public function __construct()
{
// References in case they change
// Using properties instead of globals allows the theme author to change them without affecting all templates
$this->context = &$GLOBALS['context'];
$this->settings = &$GLOBALS['settings'];
$this->scripurl = &$GLOBALS['scripurl'];
$this->txt = &$GLOBALS['txt'];
$this->user_info = &$GLOBALS['user_info'];
$this->options = &$GLOBALS['options'];
}
/**
* Shortcut function to register a subtemplate as a global subtemplate
* @param string $name
*/
public function register_subtemplate($name)
{
return Theme::getInstance()->register_global_subtemplate($name, array($this, $name));
}
/**
* Shortcut function to subtemplate
*/
public function subtemplate($subtemplate, $template = '*')
{
return call_user_func_array(array(Theme::getInstance(), 'subtemplate'), func_get_args());
}
/**
* Shortcut to get a global subtemplate
* @param string $name The subtemplate alias
* @param array $args Arguments
* @return mixed
* @throws Exception when the global subtemplate cannot be found
*/
public function __call($name, $args)
{
return call_user_func_array(array(Theme::getInstance(), 'global_subtemplate'), array_unshift($args, $name));
}
}
<?php
/**
* The theme class which holds all of the instances of the templates
* @todo This would be even better if it used lambda functions for registration so that not all of the templates need to be instantiated at the same time
*
* Not shown here is all that is required to make this a singleton.
* If there was a container class it wouldn't need to be, but that's the way it is setup now
*/
class Theme
{
/*
* Template classes
*/
protected $templates = array();
/**
* The global subtemplates
*/
protected $subtemplates = array();
/**
* @param string $alias What it will be referred to as
* @param callable $class An instance of the class
*/
public function register_template($alias, Template $class)
{
$this->templates[$alias] = $class;
}
/**
* Get all of the subtemplates of a template
* @param string $name Template alias
* @return array
*/
public function get_subtemplates($name)
{
if ($name === '*')
return $this->subtemplates;
// The template must be registered first
if (!isset($this->templates[$name]))
throw new Exception('Template "' . $name . '" not registered');
// If the template has a method for this, use that
if (is_callable($this->templates[$name]->get_subtemplates()))
return $this->templates[$name]->get_subtemplates();
else
return get_class_methods($this->templates[$name]);
}
public function get_global_subtemplates()
{
return $this->subtemplates;
}
/**
* Get an instance of the template
* @param string $class The class alias
* @return Template|false
*/
public function template($class)
{
if ($class === '*')
return false;
return isset($this->templates[$class]) ? $this->templates[$class] : false;
}
/**
* Call a subtemplate
* @param string $subtemplate The subtemplate alias/name
* @param string $template = '*' The template or * for global subtemplates
* @return mixed
*/
public function subtemplate($subtemplate, $template = '*')
{
$args = func_get_args();
$subtemplate = array_shift($args);
$template = array_shift($args);
// A global subtemplate request
if ($template === '*')
{
if (!isset($this->subtemplates[$subtemplate]))
throw new Exception('Global subtemplate "' . $subtemplate . '" not registered');
return call_user_func_array($this->subtemplates[$subtemplate], $args);
}
// The template must be registered first
if (!isset($this->templates[$template]))
throw new Exception('Template "' . $template . '" not registered');
return call_user_func_array(array($this->templates[$template], $subtemplate), $args);
}
/**
* Calls subtemplate with the global template
* @param string $subtemplate
* @return mixed
*/
public function global_subtemplate($subtemplate)
{
$args = func_get_args();
$subtemplate = array_shift($args);
if (!isset($this->subtemplates[$subtemplate]))
throw new Exception('Global subtemplate "' . $subtemplate . '" not registered');
return call_user_func_array($this->subtemplates[$subtemplate], $args);
}
/**
* Registers a global subtemplate
* @param string $name
* @param callable $callable The method callback
*/
public function register_global_subtemplate($name, $callable)
{
if (!is_callable($callable))
throw new Exception('Global subtemplate must be callable');
$this->subtemplates[$name] = $callable;
}
}
// /default/Memberlist.template.php
<?php
class Template_Memberlist extends Template
{
public function pages_and_buttons_above()
{
$extra = '
<form id="mlsearch" action="' . $this->scripturl . '?action=memberlist;sa=search" method="post" accept-charset="UTF-8">
<ul class="floatright">
<li>
<input id="mlsearch_input" onfocus="toggle_mlsearch_opt();" type="text" name="search" value="" class="input_text" placeholder="' . $this->txt['search'] . '" />
<input type="submit" name="search2" value="' . $this->txt['search'] . '" class="button_submit" />
<ul id="mlsearch_options">';
foreach ($this->context['search_fields'] as $id => $title)
{
$extra .= '
<li class="mlsearch_option">
<label for="fields-' . $id . '"><input type="checkbox" name="fields[]" id="fields-' . $id . '" value="' . $id . '" ' . (in_array($id, $this->context['search_defaults']) ? 'checked="checked"' : '') . ' class="input_check floatright" />' . $title . '</label>
</li>';
}
$extra .= '
</ul>
</li>
</ul>
</form>';
/**
* !!! Notice that here I use $this->subtemplate('pagesection'), but I could use $this->pagesection()
* !!! because I am assuming pagesection() would be registered as a global subtemplate
*/
$this->subtemplate('pagesection', 'memberlist_buttons', 'right', array('extra' => $extra));
echo '
<script><!-- // --><![CDATA[
function toggle_mlsearch_opt()
{
$(\'body\').on(\'click\', mlsearch_opt_hide);
$(\'#mlsearch_options\').slideToggle(\'fast\');
}
function mlsearch_opt_hide(ev)
{
if (ev.target.id === \'mlsearch_options\' || ev.target.id === \'mlsearch_input\')
return;
$(\'body\').off(\'click\', mlsearch_opt_hide);
$(\'#mlsearch_options\').slideToggle(\'fast\');
}
// ]]></script>';
}
/**
* Displays a sortable listing of all members registered on the forum.
*/
public function memberlist()
{
echo '
<div id="memberlist">
<h2 class="category_header">
<span class="floatleft">', $this->txt['members_list'], '</span>';
if (!isset($this->context['old_search']))
echo '
<span class="floatright" letter_links>', $this->context['letter_links'], '</span>';
echo '
</h2>
<table class="table_grid">
<thead>
<tr class="table_head">';
// Display each of the column headers of the table.
foreach ($this->context['columns'] as $key => $column)
{
// This is a selected column, so underline it or some such.
if ($column['selected'])
echo '
<th scope="col"', isset($column['class']) ? ' class="' . $column['class'] . '"' : '', ' style="width: auto; white-space: nowrap"' . (isset($column['colspan']) ? ' colspan="' . $column['colspan'] . '"' : '') . '>
<a href="' . $column['href'] . '" rel="nofollow">' . $column['label'] . '</a><img class="sort" src="' . $this->settings['images_url'] . '/sort_' . $this->context['sort_direction'] . '.png" alt="" />
</th>';
// This is just some column... show the link and be done with it.
else
echo '
<th scope="col" ', isset($column['class']) ? ' class="' . $column['class'] . '"' : '', isset($column['width']) ? ' style="width:' . $column['width'] . '"' : '', isset($column['colspan']) ? ' colspan="' . $column['colspan'] . '"' : '', '>
', $column['link'], '
</th>';
}
echo '
</tr>
</thead>
<tbody>';
// Assuming there are members loop through each one displaying their data.
$alternate = true;
if (!empty($this->context['members']))
{
foreach ($this->context['members'] as $member)
{
echo '
<tr class="', $alternate ? 'alternate_' : 'standard_', 'row"', empty($member['sort_letter']) ? '' : ' id="letter' . $member['sort_letter'] . '"', '>';
foreach ($this->context['columns'] as $column => $values)
{
if (isset($member[$column]))
{
if ($column == 'online')
{
echo '
<td>
', $this->context['can_send_pm'] ? '<a href="' . $member['online']['href'] . '" title="' . $member['online']['text'] . '">' : '', $this->settings['use_image_buttons'] ? '<img src="' . $member['online']['image_href'] . '" alt="' . $member['online']['text'] . '" class="centericon" />' : $member['online']['label'], $this->context['can_send_pm'] ? '</a>' : '', '
</td>';
continue;
}
elseif ($column == 'email_address')
{
echo '
<td>', $member['show_email'] == 'no' ? '' : '<a href="' . $this->scripturl . '?action=emailuser;sa=email;uid=' . $member['id'] . '" rel="nofollow"><img src="' . $this->settings['images_url'] . '/profile/email_sm.png" alt="' . $this->txt['email'] . '" title="' . $this->txt['email'] . ' ' . $member['name'] . '" /></a>', '</td>';
continue;
}
else
echo '
<td>', $member[$column], '</td>';
}
// Any custom fields on display?
elseif (!empty($this->context['custom_profile_fields']['columns']) && isset($this->context['custom_profile_fields']['columns'][$column]))
{
echo '
<td>', $member['options'][substr($column, 5)], '</td>';
}
}
echo '
</tr>';
$alternate = !$alternate;
}
}
// No members?
else
echo '
<tr>
<td colspan="', $this->context['colspan'], '" class="standard_row">', $this->txt['search_no_results'], '</td>
</tr>';
echo '
</tbody>
</table>';
}
public function pages_and_buttons_below()
{
// If it is displaying the result of a search show a "search again" link to edit their criteria.
if (isset($this->context['old_search']))
$extra = '
<a class="linkbutton_right" href="' . $this->scripturl . '?action=memberlist;sa=search;search=' . $this->context['old_search_value'] . '">' . $this->txt['mlist_search_again'] . '</a>';
else
$extra = '';
// Show the page numbers again. (makes 'em easier to find!)
$this->pagesection(false, false, array('extra' => $extra));
echo '
</div>';
}
}
Say I want to change Template_Memberlist::pages_and_buttons_above() with Template_Josh_Memberlist::pages_and_buttons_above() in Josh_Theme. I would simply extend Template_Memberlist and overload pages_and_buttons_above(). Obviously if there was an autoloader, I wouldn't need to include that file prior to doing that. If I want to change an entire template, but not change anything else in a theme, I can just overwrite the registration for that template with a single call. There's a lot of opportunities in this for hooks, but I didn't add them because most people would just overload the subtemplate if they wanted to do a hook. It would be nice to have type hinting but I handled what I could with exceptions.
I wrote this out in about 40 minutes in Notepad++ and I definitely didn't test it. I am just throwing the idea out there.