Skip to main content
Topic: Theme/template class (Read 11620 times) previous topic - next topic
0 Members and 1 Guest are viewing this topic.

Theme/template class

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.

Code: [Select]
<?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));
}
}
Code: [Select]
<?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
Code: [Select]
<?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'] . '" />&nbsp;
<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.
Last Edit: December 11, 2013, 08:02:32 am by groundup
Come work with me at Promenade Group

Re: Theme/template class

Reply #1

Wasting too much time on this :(

The Theme class would contain loadTheme() (rename to load()) and all of its associated functions. Wherever possible, like template_dirs, use properties instead of global variables. Maybe if necessary use both.

Template_Layers should be rewritten to extend ArrayObject and then this will all work together more nicely.

This forum is setup to only allow 20k chars in a post. Then I tried to upload the php file but it won't let me :(


Re: Theme/template class

Reply #3

Went a little farther with it. I am kind of scatter-brained so I jump from one thing to another. I didn't do this in a branch because I wasn't sure where I wanted to go and it's pretty much a total rewrite of the theme/template/view system anyway. Actually, it is the first implementation of a separation of concerns between the view and the templates. Right now the theme is the view (or vice versa).

Once this is done, Load.php and Subs.php will be way shorter. Load.php will probably be deprecated with a little more work. That's okay though, because then we need to add a Services.php will defines all of the services in the DIC. A separate Providers directory with a provider class for each controller/manager and they each define their own services would be good, but I don't know about that.

Some ideas:

Have an assets directory with js/css/images. Uploads can also go in there - maybe.

For uploads, have two directories - one protected and one unprotected. The unprotected checks if guests can see it, if so it can be served without any rewrite rules or PHP. For the protected stuff, it can go down a level and then you can only access it with PHP. If you want to be able to track the downloads, hopefully all of these classes will allow you to build a quick bootstrap file and use that. Unprotected doesn't mean that it is completely open though, it still requires a hash which is pretty hard to guess.


Re: Theme/template class

Reply #5

I had this idea that a theme author would be able to do something like $layers->advance() and it would go through all of the layers, forward and backwards until it got back to the current layer. Then it would continue on. This way you can have a single template (like index) and put $layers->advance() where the content would be.

I think I am going to implement layers as an ArrayIterator instance.

I am trying to figure out how the flow should go though...
The application/bootstrap sets up the View and sets what the theme should be
The Controller returns a ThemeResponse() which contains Language's, Template's, Layer's, and Asset's to load (used ' because they are each a class).
The Dispatcher (whatever it is called) handles the Response and tells the View to load the Templates (just the files).
The Template manager now has classes and global subtemplates and methods registered.
The View has added in the AssetManager and Translator to allow the templates to easily interact with them
Now, the view calls LayerManager::render() which does a loop of the layers (same as obExit()). Each layer consists of [(string) $template, (string) $method]
LayerManager calls TemplateManager->renderLayer() which calls the template.

Seems like a lot more than it is. The big thing is that the templates can advance on their own if they want.

Re: Theme/template class

Reply #6

First thing I'd do here is make all public methods static. Having to specify getInstance every time would be a pita... ^^ just my 2c.

Re: Theme/template class

Reply #7

There's no singletons in this and I wouldn't make any method static.

Re: Theme/template class

Reply #8

I have an idea to revive this for 1.1, keeping in mind backward compatibility and timing... and speaking of timing, it reminds me I wrote a post few days ago that is worth posting.
Bugs creator.
Features destroyer.
Template killer.

Re: Theme/template class

Reply #9

I made mention of it on the PR on GH, but I'll explain it here.

The hook calls made in https://github.com/joshuaadickerson/Elkarte/blob/View/sources/View/TemplateManager.php#L375 cannot be used. They will be called for every template, but you might it to only happen for certain templates. The solution is another system for template events.

I am thinking two tables for this - 1 for the events and 1 to link the theme and events. The template_events table should have the same as the events table from my Hooker idea and link table is just id_event and id_theme. If id_theme = 0, it is a global; id_theme = 1 is default theme. The registered events (listeners) can be cached as a serialized array in the theme settings.

When the settings are loaded, it checks for theme_events (or whatever it is called) and registers those listeners. This happens when the theme is loaded.

If you use the Hooker event system (or whatever of it) you can pretty much just use the entire thing, just change where the debug info goes and create another object with different listeners.

Re: Theme/template class

Reply #10

What do you think about adding the functions in for this for 1.1? Maybe not do all of the calls since that would be a lot of testing and might cause a lot of conflicts while we're trying to fix other things.

Re: Theme/template class

Reply #11

I'd say next round.
Change the templating "just before" the release is calling for an endless beta phase. We already did that twice this time (ILA and the parser), I think it's enough.
Bugs creator.
Features destroyer.
Template killer.

Re: Theme/template class

Reply #12

Would you guys like me to add this sort of stuff to my repository and add Travis-CI integration like you guys have?

It'd keep your repositories clean and allow for you guys to be able to quickly check build status. Thoughts?

Re: Theme/template class

Reply #13

Yep, move it to the current code would be a very good first step indeed! :D

On a somehow related note: recently @gallant mentioned about Twig, we should think about the possibility to give the option to use other template engine "easily". Well, somehow. :P
Bugs creator.
Features destroyer.
Template killer.

Re: Theme/template class

Reply #14

Quote from: emanuele – Yep, move it to the current code would be a very good first step indeed! :D

On a somehow related note: recently @gallant mentioned about Twig, we should think about the possibility to give the option to use other template engine "easily". Well, somehow. :P

OK. I'll set up a Travis CI integration for the development branch of Elkarte.