<?php
/**
* Copyright (C) 2010 Kai Dorschner
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kai Dorschner <the-hide@address.com>
* @copyright Copyright 2010, Kai Dorschner
* @license http://www.gnu.org/licenses/gpl.html GPLv3
* @package mocovi
*/
/**
* Including mocovi exception.
*/
class_exists('MvcException') or require $GLOBALS['library'].'autoload/MvcException.php';
/**
* Represents the control-master class.
*
* It is the abstract control class which serves methodes to handle data.
* A control is a complete (little) program.
*
* The execution order of the program methods is:
*
* 1. {@link load(DomNode $parent)}
* 1.1. {@link _beforeLoad()}
* 1.2. {@link _beforeCreateNode()}
* 1.2.1. {@link createNode()}
* 1.3. {@link _beforeLoadChilds()}
* 1.3.1. {@link loadChilds()}
* 1.3.1.1. {@link _beforeLoadChild()}
* 2. {@link run()}
* 2.1. {@link _beforeRun()}
* 2.2. {@link showtime()}
* 2.3. {@link _beforeRunChilds()}
* 2.4. {@link runChilds()}
* 2.5. {@link program()}
*
* To hook in and modify the default behaviour please use the underscore methods like {@link _beforeLoad()}.
* Their return value (boolean) tells whether the procedure should continue in normal order or not.
*
* @abstract
* @package mocovi
*/
abstract class Control
{
/**
* Reference of the Document Object Model set in the constructor.
*
* With this reference you are able to create, move, update or delete
* XML-elements of the page-model.
*
* @var DomDocument $dom Contains a DOM reference of the destination DOM
* @see function __construct()
* @access protected
*/
protected $dom;
/**
* Represents the own node as a part of the {@link $dom}.
*
* @var DomNode $node Contains destination node
* @see $dom
* @access protected
*/
protected $node;
protected $xpath;
/**
* Contains the node of the parent control.
*
* @var DomNode $parentNode Contains the destination parent node
* @access protected
*/
protected $parentNode;
/**
* Contains the source node from model.
*
* @var DomNode $sourceNode Contains the source node
* @access protected
*/
protected $sourceNode;
protected $sourceXpath;
/**
* This array contains all modifiers of this class.
*
* Options modify the control. You can use it as a switch.
* Nothing should be inserted HERE by authors. {@link $defaultOptions}
* "Options" is an associative array which containes
* [optionname] => (string)[option]
*
* @var Array Contains control options
* @see $defaultOptions
* @see __construct()
* @see getOption()
* @access protected
*/
protected $options = array
( 'showFromDate' => '' // used by {@link showtime()}
, 'showToDate' => '' // used by {@link showtime()}
);
/**
* Default options for the {@see program()} defined by the author.
*
* An associative array containing all default internal options.
* Authors should use THIS to define default values.
* These entries will been overwritten from the values of the array {@link $options}
*
* @var Array Contains the default control options for inheriting classes
* @access protected
*/
protected $defaultOptions = array(); // Will be overwritten in child controls.
/**
* Contains all subcontrols of this element.
*
* Every element in this array is type of {@see Control}.
*
* @var Array Contains all sub controls
* @access protected
*/
protected $controls = array();
private $insertMethods = array
( 'insertBefore'
, 'appendChild'
);
/**
* Registers the options (modifiers) for each control.
*
* You as the author have the possibility to set default options in your
* control. Therefore I had to fill these options "key by key" with
* a foreach() loop. For {@link $defaultOptions} has got the higher
* (user-level) priority it replaces all properties of {@link $options}.
*
* @param Array $options Contains the options
* @see $options
* @return void
* @access public
*/
public function __construct(DomNode $sourceNode)
{
$this->options = array_merge
( $this->defaultOptions
, $this->extractOptions($this->sourceNode = $sourceNode)
);
}
/**
* Loads destination control structure including all children.
*
* @final
* @return Control $this
* @param DomNode $parent Parent node.
*/
final public function load(DomNode $parent)
{
$this->dom = ($parent instanceof DomDocument ? $parent : $parent->ownerDocument);
$this->parentNode = $parent;
if($this->_beforeLoad($parent))
{
if($this->_beforeCreateNode($parent))
$this->createNode();
if($this->_beforeLoadChilds($parent))
$this->loadChilds();
}
return $this;
}
/**
* Loads all children when available.
*
* @final
* @return Control $this
*/
final protected function loadChilds()
{
if($this->sourceNode->hasChildNodes())
foreach($this->sourceQuery('mocovi:*|text()') as $child)
if(in_array($child->nodeType, array(XML_ELEMENT_NODE, XML_TEXT_NODE)))
{
$childControl = $this->controls[] = ControlFactory::create($child);
if($this->_beforeLoadChild($child))
$childControl->load($this->node);
}
return $this;
}
/**
* This method is called from outside to execute this control.
*
* Above all, it executes the author's function {@see program()} which
* customizes this control.
*
* @final
* @return DomNode Contains the node of this control
* @param DomDocument $dom Contains the destination DOM as reference.
* @see $dom
* @see $node
* @see showtime()
* @see createNode()
* @see runChilds()
* @see program()
* @access public
*/
final public function run()
{
if($this->_beforeRun() && $this->showtime())
{
try
{
if($this->_beforeRunChilds())
$this->runChilds(); // render childs
$this->program(); // render self node
}
catch(Exception $e)
{
$message = '';
if(version_compare(PHP_VERSION, '5.3', '>='))
{
do
$message .= (strlen($message) > 0 ? ' ---> ' : '').get_class($e).': '.$e->getMessage().' in '.$e->getFile().':'.$e->getLine();
while($e = $e->getPrevious()); // Available since PHP5.3
}
else
$message .= (strlen($message) > 0 ? ' ---> ' : '').get_class($e).': '.$e->getMessage().' in '.$e->getFile().':'.$e->getLine();
$this->error($message);
}
return $this->node;
}
}
/**
* The {@see run()} method is called in every single child of this control.
*
* By default the {@link run()} will also been executed in the sub classes,
* but you can overwrite the {@link run()} method for each control to disable
* this feature. This is reasonable when you want to create elements
* where no childelements should be contained in.
*
* @return Control $this
* @see $controls
* @see $node
* @access protected
*/
final protected function runChilds()
{
foreach($this->controls as $child)
if($this->_beforeRunChild($child))
$child->run();
return $this;
}
/**
* Adds a subcontrol into this control.
*
* @param object Contains the control which is going to be added
* @return Control This.
* @see $control
* @access public
* @deprecated
*/
// public function addControl(Control $control)
// {
// $this->controls[] = $control;
// return $this;
// }
/**
* Adds a subcontrol into this control.
*
* @access public
* @return Control new Control (not runned!)
* @param String $name
* @see $control
* @deprecated
*/
public function appendControl($name, Array $options)
{
$this->controls[] = $control = ControlFactory::createVirtual($name, $options);
return $control->load($this->node);
}
/**
* Returns the class name of this control.
*
* @return string The classname
* @access public
*/
public function getControlName()
{
return get_class($this);
}
/**
* Returns the name of the precending model element.
*
* This is realized by cutting off the suffix of the control's name
* which was appended before.
*
* @return string The precending element name.
* @access public
* @see getControlName()
*/
public function getName()
{
return substr
( $controlName = $this->getControlName()
, 0
, strlen($controlName) - strlen(ControlFactory::$controlannex)
);
}
/**
* Clones this control (deep).
*
* @return Control Cloned control.
* @access public
*/
public function getClone()
{
return ControlFactory::create($this->sourceNode->cloneNode(true));
}
/**
* jQuery-like find method to find children in any recursion depth.
*
* @return array Control set.
* @access public
*/
public function find($search = null)
{
$subControls = array();
foreach($this->controls as $subControl)
{
if(is_null($search))
$subControls[] = $subControl;
else
{
foreach(preg_split('/\s*,\s*/', $search) as $class)
{
$class .= ControlFactory::$controlannex;
if($subControl instanceof $class)
$subControls[] = $subControl;
}
// if(in_array($subControl->getName(), preg_split('/\s*,\s*/', $search)))
// $subControls[] = $subControl;
}
$subControls = array_merge($subControls, $subControl->find($search)); // Recursion
}
return $subControls;
}
/**
* Returns the value of a specified option (attribute in XML).
*
* @return string The option's value
* @access public
* @param string $name The name of the option
*/
public function getOption($name)
{
if(isset($this->options[$name])) return $this->options[$name];
if(isset($this->defaultOptions[$name])) return $this->defaultOptions[$name];
return null;
}
/**
* Returns all (user defined and default) options of this control as an associative array.
*
* @return array All options of this control.
* @access public
*/
public function getOptions()
{
return array_merge($this->defaultOptions, $this->options); // At first the defaultOptions and then override with the options.
}
/**
* Returns all default options.
*
* @return array All options of this control.
* @access public
*/
public function getDefaultOptions()
{
return $this->defaultOptions;
}
/**
* Adds text to the own node.
*
* @return Control This.
* @param string $text Contains the text for this node
* @see $node
* @access public
*/
public function addText($text)
{
$this->node->appendChild
( $this->dom->createTextNode($text)
);
return $this;
}
/**
* Checks whether this control has children controls.
*
* @return bool
* @access public
*/
public function hasChilds()
{
return (count($this->controls) > 0);
}
/**
* Replaces this control node with a new DomNode.
*
* @return bool
* @access public
* @param DomNode $newNode
*/
public function replaceNode(DomNode $newNode)
{
if($this->node instanceof DomNode)
$this->node->parentNode->replaceChild
( $newNode
, $this->node
);
elseif(empty($this->node) && $this->parentNode instanceof DomElement)
$this->parentNode->appendChild($newNode);
else
throw new MvcException('Couldn\'t replace current node. Neither the current node nor the parentNode are instanceof DomNode/DomElement');
$this->node = $newNode;
return $this;
}
public function renameNode($nodeName)
{
$newNode = $this->node->ownerDocument->createElement($nodeName);
if($this->node->attributes->length > 0)
foreach($this->node->attributes as $attribute)
$newNode->setAttribute($attribute->name, $attribute->value);
while($this->node->firstChild)
$newNode->appendChild($this->node->firstChild);
$this->replaceNode($newNode);
return $this;
}
public function sourceQuery($query)
{
if(is_null($this->xpath))
{
$this->sourceXpath = new DomXpath($this->sourceNode->ownerDocument);
$this->sourceXpath->registerNameSpace('mocovi', Mocovi::$namespace);
}
return $this->sourceXpath->query($query, $this->sourceNode);
}
public function query($query)
{
if(is_null($this->xpath))
{
$this->xpath = new DomXpath($this->dom);
$this->xpath->registerNameSpace('mocovi', Mocovi::$namespace);
}
return $this->xpath->query($query, $this->node);
}
public function error($message)
{
$this->replaceNode
(
ControlFactory::createError
( $this->getName()
, $message
)
->load($this->parentNode)
->run()
);
}
/**
* This is the authors individual function.
*
* With this function you have the possibility to create everything
* you can figure out.
* Inherited from this class and use the {@link program()} method, which is
* automatically called by {@link run()} when it's executed.
*
* Feel free - the horizon is widely opened.
*
* @return void
* @see run()
* @access protected
*/
protected function program()
{}
/**
* Tells if this program is in time and will be executed.
*
* If the current time is in the range set in the model XML, the program
* will be executed.
*
* @return bool Is it "showtime" or not?
* @see $options
* @see convertToUnixTime()
* @access protected
*/
protected function showtime()
{
if(!$this->node)
throw new Exception('You have to load control "'.$this->getName().'" first'.(isset($this->parentNode->nodeName) ? ' in '.$this->parentNode->nodeName : ''));
$from = $this->convertToUnixTime($this->options['showFromDate']);
$to = $this->convertToUnixTime
( str_replace
( '*'
, date('r', $from)
, $this->options['showToDate']
)
);
if($from <= ($now = time()) && $now <= $to)
return true;
return false;
}
/**
* Converts a datestring in unix timestamp.
*
* http://www.gnu.org/software/tar/manual/html_chapter/Date-input-formats.html#SEC115
*
* @return integer Returns either the converted time or the current time
* @param string $userdate Contains the the user time format
* @see splitDate()
* @access protected
*/
protected function convertToUnixTime($userdate)
{
if(empty($userdate))
return time(); //now
return strtotime($userdate);
}
/**
* Creates the own node and returns it.
*
* @return Control This.
* @see nodeCreated()
* @see $node
* @see $dom
* @access protected
*/
final protected function createNode($insertMethod = 'appendChild')
{
if(!in_array($insertMethod, $this->insertMethods))
$insertMethod = 'appendChild';
$this
->parentNode
->$insertMethod
( $this->node = $this->dom->createElement
( $this->getName()
)
);
return $this;
}
protected function deleteNode()
{
$this->replaceNode(new DomText(''));
return $this;
}
/**
* Returns whether the own node already has been created.
*
* @access public
* @return bool Am I created or not?
* @see $node
*/
final public function nodeCreated()
{
return ($this->node instanceof DomElement);
}
/**
* Applies an attribute/option from the model element to the output XML.
*
* @return Control This.
* @see getOption()
* @access protected
*/
protected function adoptAttribute($option)
{
if($value = $this->getOption($option))
$this->setAttribute
( $option
, $value
);
return $this;
}
protected function setAttribute($name, $value)
{
$this->node->setAttribute
( $name
, $value
);
return $this;
}
/**
* Applies multiple attributes/options from the model element to the output XML.
*
* @return Control This.
* @see adoptAttribute()
* @access protected
*/
protected function adoptAttributes(/*args*/)
{
foreach(func_get_args() as $arg)
$this->adoptAttribute($arg);
return $this;
}
/**
* Applies all attributes/options from the model element to the output XML.
*
* @return Control This.
* @see adoptAttribute()
* @access protected
*/
protected function adoptAllAttributes()
{
foreach($this->getOptions() as $name => $option)
if(!in_array($name, array('showFromDate', 'showToDate')))
$this->adoptAttribute($name);
return $this;
}
private function extractOptions(DomNode $node)
{
if(isset($node->attributes))
foreach($node->attributes as $child)
$this->options[$child->name] = $child->value;
return $this->options;
}
protected function _beforeLoad(DomNode $parent)
{
return true;
}
protected function _beforeLoadChilds(DomNode $parent)
{
return true;
}
protected function _beforeLoadChild(DomNode $child)
{
return true;
}
protected function _beforeCreateNode(DomNode $parent)
{
return true;
}
protected function _beforeRun()
{
return true;
}
protected function _beforeRunChild(Control $child)
{
return true;
}
protected function _beforeRunChilds()
{
return true;
}
}