What a Component Would be In Real World?
In the last blog, I've just explored the pure principles and theories about strict MVC design of a Joomla! component. However, in real world, things always change. You will find that principles are just rough guidances, you don't have to confine yourself to them.
Copy a Shadow for Examination
I am not sure to what extent the change I will make to the current web application, and just for backup purpose, I decide to make a shadow copy and work on that copy.
First of all, we need to copy the content of Joomla! folder into a new created folder, say, soccer. And dump the data of the database in phpMyAdmin:
just keep the setting as default, and get a .sql file downloaded.
Secondly, create new database, with new dedicated user, grant the priveleges. And suppose the profile:
dbname: dbname
username: username
pwd: password
Then log into the admin panel of the new web application by accessing new url: localhost/soccer. Go to Global Config->Server, fill in the new data:
And the final step is to open the configuration.php file to change the $password to your password.
That's done! All the following operations will be performed on the new copy.
How does Joomla! determine which views/layouts to show when specifying a menu item's type
In addition to where does Joomla! store the component menu items data, this is the one problem that I am most eager to solve. Joomsport offers six views with one default layout for menu item:
It seems to be a complex question, since no one answered me so far in Joomla! forum. So I have to figure out the logic on my own. It's not really hard, the url is a very useful clue:
http://localhost/soccer/administrator/index.php?option=com_menus&task=type&menutype=mainmenu&cid[]=27&expand=joomsport
The component to handle that job is com_menus, so open:
/administrator/components/com_menus/admin.menus.php:
$controller = new MenusController( array('default_task' => 'viewMenus') );
$controller->registerTask('apply', 'save');
$controller->execute( JRequest::getCmd( 'task' ) );
$controller->redirect();
what it did is just pass control to main controller, then go to
/administrator/components/com_menus/controller.php:
and the task parameter in url is type, so find the type method:
function type()
{
JRequest::setVar( 'edit', true );
$model =& $this->getModel( 'Item' );
$view =& $this->getView( 'Item' );
$view->setModel( $model, true );
// Set the layout and display
$view->setLayout('type');
$view->type();
}
which tells that, the view and model called is Item, and the layout is type.
open: /administrator/components/com_menus/views/item/view.php
theoretically, the class MenusViewItem in this file should call the counterpart model method to get data:
function type($tpl = null)
{
......
// Initialize variables
$item = &$this->get('Item');
$expansion = &$this->get('Expansion');
$component = &$this->get('Component');
$name = $this->get( 'StateName' );
$description = $this->get( 'StateDescription' );
$menuTypes = MenusHelper::getMenuTypeList();
dump($item, 'item');
dump($expansion, 'expansion');
dump($component, 'component');
// Set document title
if ($item->id) {
$document->setTitle(JText::_( 'Menu Item' ) .': ['. JText::_( 'Edit' ) .']');
} else {
$document->setTitle(JText::_( 'Menu Item' ) .': ['. JText::_( 'New' ) .']');
}
$this->assignRef('item', $item);
$this->assignRef('components', $components);
$this->assignRef('expansion', $expansion);
parent::display($tpl);
}
It assign three variables: item, components and expansion, which means the views data must be stored in one of them, so I add dump() function to dump these three variables and the result:
from the data nature, &expansion contains the tree HTML code as its first element. So let's find out how expansion its value is gained:
$expansion = &$this->get('Expansion');
this statement actually call MenusModelItem method &getExpansion. So open: /administrator/components/com_menus/models/item.php
function &getExpansion()
{
$item = &$this->getItem();
$return['option'] = JRequest::getCmd('expand');
$menutype = JRequest::getVar('menutype', '', '', 'menutype');
if ($return['option'])
{
require_once(JPATH_ADMINISTRATOR.DS.'components'.DS.'com_menus'.DS.'classes'.DS.'ilink.php');
$handler = new iLink($return['option'], $item->id, $menutype);
$return['html'] = $handler->getTree();
return $return;
} else {
$return['html'] = null;
}
return $return;
}
It calls iLink method getTree to get the HTML code, then open /administrator/components/com_menus/classes/ilink.php, and find the method getTree:
function getTree()
{
$depth = 0;
$this->reset();
$class = null;
// Recurse through children if they exist
while ($this->_current->hasChildren())
{
$this->_output .= '<ul>';
$children = $this->_current->getChildren();
for ($i=0,$n=count($children);$i<$n;$i++)
{
$this->_current = & $children[$i];
$this->renderLevel($depth,($i==$n-1)?1:0);
}
$this->_output .= '</ul>';
}
return $this->_output;
}
This piece of code doesn't give us anything, but take a look at its constructor:
function __construct($component, $id=null, $menutype=null)
{
parent::__construct();
if ($id) {
$this->_cid = "&cid[]=".$id;
} else {
$this->_cid = null;
}
if ($menutype) {
$this->_menutype = "&menutype=" . JFilterInput::clean($menutype, 'menutype');
} else {
$this->_menutype = null;
}
$this->_com = preg_replace( '#\W#', '', $component );
// Build the tree
if (!$this->_getOptions($this->_getXML(JPATH_SITE.'/components/com_'.$this->_com.'/metadata.xml', 'menu'), $this->_root))
{
if (!$this->_getViews())
{
// Default behavior
}
}
}
The core code is the call to $this->_getOption, and it invokes $this->_getXML():
function _getXML($path, $xpath='control')
{
dump($path, 'path');
// Initialize variables
$result = null;
// load the xml metadata
if (file_exists( $path )) {
$xml =& JFactory::getXMLParser('Simple');
if ($xml->loadFile($path)) {
if (isset( $xml->document )) {
$result = $xml->document->getElementByPath($xpath);
}
}
return $result;
}
return $result;
}
Above the first line, I put a dump function, to trace out the xml file's path:
So, at this point, although I've not clearify the recursive calls and how the XML data be parsed and extracted, we can see that all the XML are under the views' folders which appear in the 'Change Menu Item Type' panel, so we can sum up at the point: Joomla! detect the views by finding its metadata XML file, if it has, then that would be shown in the 'Change Menu Item Type' panel.
The Relation between Metadata Description & Admin Panel:
view:
layout:
How does the metadata file result in the basic parameter setting panel on the left(as shown in the above shot) is my biggest discovery today. But I did the exam on team view instead.
Go to file: /components/com_joomsport/views/blteam/tmpl/default.xml
<state>
<name>Team Layout</name>
<description>Team Layout</description>
<url addpath="/administrator/components/com_joomsport/elements">
<param name="sid" type="season" default="0" label="Select Season" description="Season" />
<param name="tid" type="team" default="0" label="Select Team" description="Team" />
</url>
<params>
</params>
</state>
There's one line specifies the path: /administrator/components/com_joomsport/elements, which should tell Joomla! where to look for some file, and the directory is:
And the attribute 'type' specifies season for one parameter and team for the other, those names may match the php files under the element directory. Let's check out that, open team.php:
<?php
defined('_JEXEC') or die( 'Restricted access' );
class JElementTeam extends JElement
{
var $_name = 'team';
function fetchElement($name, $value, &$node, $control_name)
{
global $mainframe;
$db =& JFactory::getDBO();
$doc =& JFactory::getDocument();
$template = $mainframe->getTemplate();
$fieldName = $control_name.'['.$name.']';
$article->title = '';
if ($value)
{
$query = "SELECT * FROM #__bl_teams WHERE id=".$value;
$db->setQuery($query);
$rows = $db->loadObjectList();
if(isset($rows[0]))
{
$row = $rows[0];
$article->title = $row->t_name;
}
}
else
{
$article->title = JText::_('Select Team');
}
$js = "
function jSelectArticle(id, title, object) {
document.getElementById(object + '_id').value = id;
document.getElementById(object + '_name').value = title;
document.getElementById('sbox-window').close();
}";
$doc->addScriptDeclaration($js);
$link = 'index.php?option=com_joomsport&task=team_menu&tmpl=component&object='.$name;
JHTML::_('behavior.modal', 'a.modal');
$html = "\n".'<div style="float: left;"><input style="background: #ffffff;" type="text" id="'.$name.'_name" value="'.htmlspecialchars($article->title, ENT_QUOTES, 'UTF-8').'" disabled="disabled" /></div>';
// $html .= "\n <input class=\"inputbox modal-button\" type=\"button\" value=\"".JText::_('Select')."\" />";
$html .= '<div class="button2-left"><div class="blank"><a class="modal" title="'.JText::_('Select Team').'" href="'.$link.'" rel="{handler: \'iframe\', size: {x: 650, y: 375}}">'.JText::_('Select').'</a></div></div>'."\n";
$html .= "\n".'<input type="hidden" id="'.$name.'_id" name="'.$fieldName.'" value="'.(int)$value.'" />';
return $html;
}
}
It is a little bit complcated with first glance, basically, it defines a class, with one method, but the last line of the method tells that it returns a piece of HTML code to its caller, and that HTML code begins with '<div style=...' and end in a hidden input tag. If this piece of code appears in the page of editing menu item, then we can confirm the relation between the parameter tag in xml file and the panel. For exactly getting the returned code, I added a dump() function call before it returns.
And in the admin page, use Chrome tools to interrogate the HTML piece:
The code just matchs exactly!!!
And how was the popup window generated?
It is actually realized by using iframe tag. And the src attribute is a link, which is the same ashref of the parameter button 'Select Team'. And open the link in a new tab:
The javascript function is just added by fetchElement method of JElementclass:
$js = "
function jSelectArticle(id, title, object) {
document.getElementById(object + '_id').value = id;
document.getElementById(object + '_name').value = title;
document.getElementById('sbox-window').close();
}";
$doc->addScriptDeclaration($js);
And the link is generated:
$link = 'index.php?option=com_joomsport&task=team_menu&tmpl=component&object='.$name;
Finally, it assemblys all the HTML pieces and these variables:
$html = "\n".'<div style="float: left;"><input style="background: #ffffff;" type="text" id="'.$name.'_name" value="'.htmlspecialchars($article->title, ENT_QUOTES, 'UTF-8').'" disabled="disabled" /></div>';
$html .= '<div class="button2-left"><div class="blank"><a class="modal" title="'.JText::_('Select Team').'" href="'.$link.'" rel="{handler: \'iframe\', size: {x: 650, y: 375}}">'.JText::_('Select').'</a></div></div>'."\n";
$html .= "\n".'<input type="hidden" id="'.$name.'_id" name="'.$fieldName.'" value="'.(int)$value.'" />';
Then just return it.
Examing the Frontend Output of Joomsport
Remember the suppppper simple Joomla! template I created a couple of weeks ago?! It comes to be handy now, let's use it to inspect each view/layout its output of joomsport, and then map them to the PHP code behind.
First thing I want to point out is the mechanism of how Joomla! component handle tasks, the theory says that, the methods of main controller class correspond to tasks, the name of method is exactly the same as the task it is supposed to handle, so if you have set task=match in your URL, then Joomla! will seek match() method in controller class difination and run it. There are some preset tasks, such as display, edit, save, delete and so on.. And display is default.
But the observation of Joomsport is:
In fact, the URL of the menu item will never contains parameter 'task', whereas in the main controller class of joomsport, there are five methods(we ignore the last one for a while):
So, the question is will the method be invoked and in what case if yes?
A trial:
add test code to view_match method:
function view_match()
{
echo 'view match task';
JRequest::setVar( 'view', 'match' );
parent::display();
}
And access: http://localhost/soccer/index.php?option=com_joomsport&task=view_match&id=1 (which is not generated by menu item, its typed manually)
in my Chrome:
This result tells us the view_match method did be invoked. But what this method did is just setting the view parameter, and supposed that task parameter is not set and the view is set to 'match', then the method display will take charge because of default task is display, and let's take a look at its body:
function display()
{
//echo 'display'; //test go here by default
$view = JRequest::getCmd( 'view' );
//var_dump($view); //test 'ltable'
if(!$view)
{
$view = 'ltable';
}
JRequest::setVar( 'view', $view );
$cal = JRequest::getCmd( 'layout' );
//var_dump($cal); //test empty
if($cal == 'calendar')
{
JRequest::setVar( 'view', 'calendar' );
JRequest::setVar( 'layout', 'default' );
}
parent::display();
}
In this function, if the view parameter is set, it will load it, otherwise load ltable view. So, we do not have to bother with setting task when setting view is enough. Finally, the other methods in main controller are redundant utterly.
Refs:
http://docs.joomla.org/Adding_view_layout_configuration_parameters