français

a/X développement web 2

Download HoinP
release 0.4 (v 62)

Introduction

HoinP means HTML objects in PHP. The easy spelling is HoP.

It's free software under LGPL

The purpose of this library is to offer a structured way to generate XHTML markup from PHP, without the traditional interleaving of PHP chunks, HTML chunks and echo statements all over.

It is an intermediate concept between templating systems (I don't like the paradigm) and PHP DOM extensions (over complicated for the purpose). It will fit well in MVC designed applications and is close to the definition of helpers in the Zend Framework. In fact, the first draft of this library is older than the ZF 1.0 release, while it shares some conceptual similarities with these helpers.

If you hesitate to use it due to server load, consider caching methods and get the flexibility at low cost.

Requirement

HoinP requires PHP 5

Quick usage overview

In the controler code :

<?php
require_once 'hop/hop.php' ;			// the single include required, classes are autoloaded

$a_select = $hop->select('choice_1')		//create a select object with id 'choice_1'
	->auto_name()				//the name attribute will be from the id
	->add($hop->option())			//empty option object
	->add($hop->option('date')		//create and add an option object
		->value(date('Y')))		//set the value of this option : a PHP fonction result
	->add($hop->option('time')		//second option
		->value(time()))		//its value
	->onchange('this.form.submit()')	// add an onchange attribute to the a_select
	->class('a_class')			// and a class
	 ;
?>
	

In the view (or theme) file :

<form action="">
	<p><?php $a_select->render() ?></p>
</form>
	

A cooler syntax, using Hop_node_from_file class (see below) :

<form action="">
	<p>{$a_select->render()}</p>
</form>
	

And you get :

Yes, this is useless for such a simple case…

Only a few classes

API

General rule : every method returns $this, the object it pertains to, unless it is obviously inappropriate, as for getter methods.

Hover the cursor on a class name to unfold relevant chapter. Slide to left margin to fold in. Disable this crazy feature ! (Works badly on IE6, the hover syndrome, hehe)

Base Classes

Hop_node

This the base class. You will instantiate it, each time you need an abstract container (i.e. with no rendering by itself), for example to return a single node from a method or function, with html tree inside.

It defines all methods pertaining to element tree

See source file

hop_node.php

<?php
/*
« Copyright 2007 Max Barel a_x@ac-mb.info »
Release : 0.4 ($Rev : 84 $)
$Date : 2009-05-14 21:24:32 +0200 (Jeu, 14 mai 2009) $

This file is part of HoP.

HoP is free software ; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation ; either version 2.1 of the License, or
(at your option) any later version.

HoP 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with HoP ; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

/*
This simple class define a base container, which has no proper output rendered, only contained nodes (or elements).
It is the base of the Hop_element class.
It must be used per see, each time one need an abstract container to hold contents.
*/
class Hop_node {
public $container ; //for nesting management
public $content = array() ; // the content of the object, rendered by the render() method

public function __clone() {
	$this->container = null;
	foreach ($this->content as $i => $el) if ( $el instanceof Hop_node ) {
		$this->content[$i] = clone $el;
		$this->content[$i]->set_container($this);
	}
}

protected function set_container($node) {
	//this function can be redefined in descendant class to supplement the linking task (e.g. from container to child)
	$this->container = $node;
}

function cut() {
	if ($this->container) {
		foreach($this->container->content as $i => $o) if ($o === $this) unset($this->container->content[$i]);
		$this->container = null;
	}
	return $this ;
}

//setter
//these three functions add element to content
function set_content($el = null) {
	if ( is_array($el) ) $this->content = $el;
	elseif ($el !== null) $this->content = array($el);
	foreach ($this->content as $el)
		if ( $el instanceof Hop_node ) $el->set_container($this);
	return $this ;
}
function add($el = null) {
	if ($el !== null) $this->content[] = $el; //allow any type, only string and Hop_node are rendered, everything else is casted to string
	if ( $el instanceof Hop_node ) $el->set_container($this);
	return $this ;
}
function insert($el = null) {
	if ($el !== null) array_unshift($this->content, $el);
	if ( $el instanceof Hop_node ) $el->set_container($this);
	return $this ;
}
function insert_at($pos, $el = null) {
	if ($el !== null) array_splice ( $this->content , $pos , 0 , array($el) );
	if ( $el instanceof Hop_node ) $el->set_container($this);
	return $this ;
}
public function insert_before($el = null) {
	$i = $this->index();
	$this->container->insert_at($i, $el);
	return $this ;
}
public function insert_after($el = null) {
	$i = $this->index() + 1;
	if (!$i) $i = count($this->container);
	$this->container->insert_at($i, $el);
	return $this ;
}

//getter
//commodity method. return the last element of content array
function last() { return end($this->content); }
//commodity method. return the first element of content array
function first() { return reset($this->content); }
function container() { return $this->container; }
//index of this element
public function index() {
	if ($this->container) foreach ($this->container->content as $i=>$el) if ($el === $this) return $i;
	return 0 ;
}

//render this element, recursively
function render($indent = '') {
	$html_string = '' ;
	if ($this->content and !is_array($this->content)) $this->content = (array) $this->content; //in case the content has been overwritten with a string
	foreach ($this->content as $el) {
		if ( $el instanceof Hop_node ) $html_string .= $el->render($indent);
		elseif (is_string($el)) $html_string .= (string) $el ;
		else $html_string .= print_r($el, true) ; //debug purpose don't rely on it
	}
	return $html_string ;
}

}
?>

Hop_element

This is the core class of the package. You will probably not instantiate it directly very often, as the Hop class below offer a useful wrapping factory. Nevertheless it's important to grab some key points of its design.

First, every Hop_element must have an id. If none is provided, it is generated by concatenating its type string and the current number of element in the pool (i.e. p_43). It is important to note that this id is in the PHP space, not the html one, even if it's possible to have the html id reflect the object id by setting the auto_id property. Alternatively setting the auto_name property will reflect the id to the name html attribute.

This id is very useful to access an element after it has been created, using a path notation :

$container->add(new Hop_element('div', 'div_id'));	//create a div element inside an existing container object
$container->div_id->auto_id();				//access the new div as a container property, using its id

There is also a static property (i.e. unique and global to the class) named pool to access any element by its id, wherever it is in the tree :

Hop_element::pool->div_id;	//access through the class static property
$hop->pool('div_id');		//access through the $hop object getter (see below)
$hop->pool()->div_id;		//alternate syntax

This implies that every id must be unique. If it's not it will be rejected and an automatic id generated. Beware, while using pool access, to address the right object. Path access is safer in this respect : a rejected id will be inaccessible. HTML also enforce unique id so it is an early warning.

Methods :

See source file

hop_element.php

<?php
/*
« Copyright 2007 Max Barel a_x@ac-mb.info »
Release : 0.4 ($Rev : 77 $)
$Date : 2008-12-25 19:09:27 +0100 (Jeu, 25 déc 2008) $

This file is part of HoP.

HoP is free software ; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation ; either version 2.1 of the License, or
(at your option) any later version.

HoP 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with HoP ; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

//XHTML element.
class Hop_element extends Hop_node {
const INDENT = "\t";		//define the indentation string	
public static $comment_tag = true ;	//optional comment at closing tag for readability. Global to the whole rendering
public static $pack = false ;	//pack the html

//singleton
protected static $n_el = 0 ;	//element count in the pool added for time optimization
public static $pool ;		//a "flat" container object for all Hop_elements.
//Use it to direct access to an object : Hop_element::$pool->object_id
//See also the commodity function in Hop class

//mandatory properties values
public $type ;				//string, element type eg. 'div','p'
public $id ;				//id of this object, automatic generation if not given
//optional properties
public $attributes = null ;	//stdclass object : html attributes container
public $auto_id = false ;	//should 'id' html attribute be reflecting the object id
public $auto_name = false ;	//should 'name' html attribute be reflecting the object id (eg. for input element)
public $empty_el = false ;	//is this element type empty (otional empty should be set to false)
public $nl = 1 ;				//newlines when rendering : 0 for inline (e.g. <span>),
							// 1 for newline before the opening tag (one line elements : <p> or <legend>),
							// 2 for newline before the closing tag (container elements :  <body>, <div>)

//constructor
function __construct($type, $options = null) {
	$this->type = $type; // set the element type
	//process options
	if (is_string($options)) $options = array('id' => $options) ; // a string option default to the object id
	elseif ($options and !is_object($options)) $options = (object) $options ; //cast to object for uniform processing
	if (isset($options->content) ) {
		//content should be handled by setter for correct parent linking
		$this->set_content($options->content);
		unset ($options->content);
	}

	if ($options) foreach ($options as $k => $v) $this->$k = $v; //make other options properies of the element
	//if (isset($this->$k)) 
	
	//cast attributes to object for easy access (might be array or simple string)
	if ($this->attributes and !is_object($this->attributes)) $this->attributes = (object) $this->attributes;
	// if ($this->attributes and !is_object($this->attributes)) settype($this->attributes, 'object');

	self::$n_el ++ ;
	//setting id : if id is specified it's used else a new one is generated in set_id()
	// a side effect is that an existing element with same id becomes unreachable from the pool (in a loop typically).
	// One should previoulsy re-set_id() on such element to be able to reach it.
	// the three line below solve the problem but force to avoid looping on same id.
	// Not enforced yet for compatibility.
	// $id = $this->id;
	// $this->id = null;
	// $this->set_id($id);
	$this->set_id($this->id);
}

function __clone() {
	parent::__clone() ;
	self::$n_el ++ ;
	$id = "{$this->id}_clone_" . self::$n_el ;
	$this->id = $this->container = null;
	$this->set_id($id);
	if ($this->attributes) $this->attributes = clone $this->attributes;
}

//id setter. useful for cloned element or changing an automatic id
function set_id($id = null) {
	if ($this->id) {
		unset(self::$pool->{$this->id});
		if ($this->container) unset($this->container->{$this->id});
	}
	$this->id = $id;
	//if no id generate one, for the safeness of the pool. The element will then be difficult to address.
	if (!$this->id or !is_string($this->id)) $this->id = $this->type;
	if (isset(self::$pool->{$this->id})) $this->id = "{$this->type}_" . self::$n_el ;
	//add self to the pool
	self::$pool->{$this->id} = $this;
	if ($this->container) $this->container->{$this->id} = $this;
	return $this ;
}

protected function set_container($node) {
	//redefined to add a reverse link from parent to child through an id named property
	parent::set_container($node) ; //=> $this->container = $node;
	// if (is_a($node, __CLASS__))
	$node->{$this->id} = $this; //cannot be done at node level becaus nodes have no id
}
	
function cut() {
	unset($this->container->{$this->id});
	return parent::cut() ;
}

//rendering function.
function render($indent = '') {
	$nl = self::$pack ? 0 : $this->nl;
	$html_string = '' ;
	if ($this->auto_id and empty($this->attributes->id) ) $this->set_attr('id', $this->id);
	if ($this->auto_name and empty($this->attributes->name) ) $this->set_attr('name', $this->id);
	if ($nl) $html_string .= "\n$indent";
	$html_string .= "<$this->type";
	if ($this->attributes) {
		//cast in case it has been set to a string
		if (!is_object($this->attributes)) $this->attributes = (object) $this->attributes;
		foreach ($this->attributes as $k => $value) {
			if ($k == 'raw' or $k == 'scalar') $html_string .= " $value"; //for raw formatted attributes
			else $html_string .= " $k=\"$value\"";
		}			
	}
	if ($this->empty_el) {
		$html_string .= ' />';
	} else {
		$html_string .= '>';
		$html_string .=  parent::render(self::INDENT . $indent) ;
		if ($nl > 1) $html_string .= "\n$indent";
		$html_string .= "</$this->type>";
		//optional, for code documentation
		if ($nl > 1 and self::$comment_tag and isset($this->attributes->id)) $html_string .= "<!-- {$this->attributes->id} -->";
	}
	return $html_string ;
}

//properties setter
function auto_id($val=true) { $this->auto_id = $val; return $this ; }
function auto_name($val=true) { $this->auto_name = $val; return $this ; }
function empty_el($val=true) { $this->empty_el = $val; return $this ; }
function nl($val=1) { $this->nl = $val; return $this ; }

//commodity setter for html id
function id($value = null) {
	if ($value) $this->set_attr('id', $value); //set html id attribute, no incidence on hop id
	else $this->auto_id = true; // shortcut to use hop id
	return $this ;
}	
//commodity setter for html name
function name($value = null) {
	if ($value) $this->set_attr('name', $value); //set html name attribute
	else $this->auto_name = true; // shortcut to <auto_name></auto_name>
	return $this ;
}

//commodity function to set an attribute
function set_attr($attr, $value=null) {
	if (isset($value)) $this->attributes->$attr = htmlspecialchars($value); //htmlspecialchars htmlentities
	return $this ;
}
//commodity function to get an attribute
function get_attr($attr) {
	return @$this->attributes->$attr;
}
//remove an attribute
function remove_attr($attr) {
	unset ($this->attributes->$attr);
	return $this ;
}

//generic attibute setter
private function __call($setter, $params) {
	if (ereg('(set|get|remove)_(.*)', $setter, $e_res)) {
		switch ($e_res[1]) {
			case 'set' : return $this->set_attr($e_res[2], $params[0]);
			case 'get' : return $this->get_attr($e_res[2]);
			case 'remove' : return $this->remove_attr($e_res[2]);
		}			
	}
	//shortcut setter : $el->class('a_class') <=> $el->set_class('a_class') <=> $el->set_attr('class', 'a_class')
	else return $this->set_attr($setter, $params[0]);
	// else throw new Exception("invalide function call : $setter");
}

//specific class methods
public function add_class($class = null) {
	if ($class) $this->set_attr('class', (($c = $this->get_attr('class')) ? "$c " : '') . "$class");
	return $this ;
}

public function remove_class($class = null) {
	if ($class) $this->set_attr('class', trim(ereg_replace(" *$class", '', $this->get_attr('class'))));
	return $this ;
}
}
?>

Factory

Hop

The file containing this class is the only one you should include to use HoinP. It sets the include path and the autoload function to load other classes on the fly.

This class is a singleton. You can't instantiate object with the new operator. You must do it through the single method which always return the same single instance. The global $hop object is created by default when the file is included. If you don't like this identifier or don't want it at global level, unset it and get another reference :

unset($hop) ;
$my_prefered_id_in_any_name_scope = Hop::single() ;

The main purpose of this class is to define a factory for Hop_element, easing the library usage.
This is done through a fallback method, allowing creation of elements of any kind :

$hop->canvas();
$hop->whatever_you_want();

Nevertheless, this feature is controlled by mean of three properties arrays, defining collections of elements : containers, single_line, in_flow. They are mutually exclusive, an element type should be listed in only one array. By the default setting, single_line is empty, and thus any element type not listed elsewhere is considered of this category. These categories, which do not map exactly to block and inline html notions, are used to set up the way element are rendered. This is a formating issue, with no functional consequence. See the source comments for detail on output formatting. They are also used to define the parameter ordering and this can be confusing at first.

A last collection, empy_el lists empty elements and is not exclusive : an empty element can be single_line, the default, or in_flow.

Redefine these arrays (with properties assignment, not source editing) to adapt formatting to your liking, or to exclude exotic elements.

Parameters order

Hop fallback factory method accepts 2 parameters : options and content.
As containers are more likely to have id, be subtree root and have content added subsequently, options/id is the first parameter.
Single_line and in_flow elements are most often used to add immediate content, thus it is the first parameter.

Example :
$branch = $hop->div('branch_id')				// a div with an id, no content
	->auto_id()						// html id
	->add($hop->p('this the text content of the paragraph'))//add a p to the div with content, no id nor options
	 ;
$div_op = $hop->div(null, $hop->p('blah'));			//a div with no id and immediate content
$div_op->add($hop->p(null, 'p_id'));				//add another p with no content and an id, to the div
$div_op->p_id->add('eventually a content');			//add content to the p latter in the process

All examples use simple strings (id) as options parameter, but it can be structured, as seen before in hop_element chapter.

pool() method

This is a convenient alternate method to access the global pool. Not much shorter but easy for flow typing.

$hop->pool('div_id');		//access through the $hop object getter
$hop->pool()->div_id;		//alternate syntax
Hop_element::pool->div_id;	//equivalent access through the class static property

See source file

hop.php

<?php
/*
« Copyright 2007 Max Barel a_x@ac-mb.info »
Release : 0.4 ($Rev : 69 $)
$Date : 2008-03-16 17:28:51 +0100 (Dim, 16 mar 2008) $

This file is part of HoP.

HoP is free software ; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation ; either version 2.1 of the License, or
(at your option) any later version.

HoP 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with HoP ; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/
/*
==================================
library include and autoload
==================================
*/
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . PATH_SEPARATOR . dirname(__FILE__) . '/widgets') ;

function __autoload($class_name) { require_once strtolower($class_name) . '.php' ; }

/*
This class is a commodity one, which encapsulate any useful method not pertaining to nodes classes.
It is designed have only one instance, created after the class declaration as the global variable $hop.
This is the only identifier of the HOP library in the global namespace.
The most useful feature is a trap made with the __call magic method. This offers an automatic html objects creation for every type :
$hop->div() to instantiate a div, and so on.
Default setting allow creation of any arbitrary element, even invalid like $hop->menu() etc.
To restrict the element creation to valid types, set the $hop->single_line property to the relevant list o types.

==================================
container elements
==================================
Element of this kind are rendered with a newline before both opening and closing tag :
<div>
	<div>
	</div>
</div>
As content is more likely to be added with add() or insert methods, it is the second (optional) parameter.
$d = $hop->div('div1','initial content')->add('added content');
The first parameter can be either a string and then interpreted as the object id or an array or object of properties of the new object.
==================================
One line elements (default)
==================================
Rendered on one line :
<p>text</p>
The content parameter it the first : $p = $hop->p('text', $options).
Content adding methods are still usable : $p->add('another text')->add(span('special text')->set_class('special'));
==================================
Inflow elements
==================================
Rendered in flow, without any newline :
text before<span>special text</span>text after
The content parameter it the first : $sp = $hop->span('special text', $options).
Content adding methods are still usable : $sp->add('another text')->set_class('special'));		
==================================
Empty elements
==================================
There is no content so the only, optional parameter is id/options :
$inp = $hop->input('txt1')->set_type('text')->set_value('default value'); //flow way
$inp = $hop->input(array('id'=>'txt1', 'attributes' => array('type' => 'text', 'value' => 'default text'))) ; // options way
*/
class Hop {
private static $single_hop ;
private function __construct() {}

static function single() {
	$c = __CLASS__ ;
	if (!self::$single_hop) self::$single_hop = new $c ;
	return self::$single_hop ;
}

// Following properties can be set to different tag list to adapt rendering to your preference.
// The first three are exclusives to each other
public $container = array('html', 'head', 'body', 'div', 'ul', 'ol', 'dl', 'pre', 'blockquote', 'object', 'table', 'caption', 'tbody', 'tr', 'form', 'fieldset', 'select') ;
public $single_line = null ;
public $in_flow = array('span', 'a', 'em', 'strong', 'dfn', 'code', 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'sub', 'sup', 'ins', 'del') ;
// This property define empty elements and should not be changed unless set to another markup.
public $empty_el = array('br', 'hr', 'base', 'meta', 'link', 'param', 'area', 'img', 'input', 'col') ;

//auto named elements
public $auto_name = array('input', 'select', 'textarea') ;
//auto id elements (useful for label to form elements)
public $auto_id = array() ; //'input select textarea' ;

private function __call($element_type, $params) {
	$params[] = null ; $params[] = null ; //add null values to ensure the minimum params number
	//default settings
	$nl = 1 ;
	$options = $params[1] ;
	$content = $params[0] ;
	$check_valid = false ;
	if (in_array($element_type, $this->container)) {
		$options = $params[0] ; // id/options is first parameter for container
		$content = $params[1] ;			
		$nl = 2 ;
		$check_valid = true ;
	} elseif (in_array($element_type, $this->empty_el)) {
		if (is_string($params[0])) $options->id = $params[0];
		else $options = (object) $params[0] ; // id/options is the sole parameter for empty elements
		$options->empty_el = true;
		$content = null ;
		$check_valid = true ;
	}
	if (in_array($element_type, $this->in_flow)) {
		$nl = 0 ;
		$check_valid = true ;
	}
	if ($check_valid or !$this->single_line or in_array($element_type, $this->single_line)) {
		$el = new Hop_element($element_type, $options) ;
		$el->set_content($content)->nl($nl);
		if (in_array($element_type, $this->auto_name)) $el->auto_name(true);
		if (in_array($element_type, $this->auto_id)) $el->auto_id(true);
		return $el ;
	} else throw new Exception("undefined tag : $element_type");
}

/*
	This function return a hop_element object given its id. Or the global pool if none.
	$o = $hop->pool('anID');
	$o = $hop->pool()->anId;
	$o = Hop_element::$pool->$id;
*/
function pool($id = null) {
	if ($id) return @Hop_element::$pool->$id; // isset(Hop_element::$pool->$id) ? Hop_element::$pool->$id :null;
	return Hop_element::$pool ;
}

function clone_el($el) {
	if (is_string($el)) $el = $this->pool($el);
	return clone $el ;
}
}

$hop = Hop::single() ;

?>

Interface classes

Hop_form_controller

This class works at the interface level between controllers and views. It's a wrapper for creating html form elements and, moreover set and get values of these elements from request or object. It also acts as a container for values, before the elements are generated.

Example (from the source file comments) :
$fc = new Hop_form_controller( array(
	array('txt1', "input()->type('text')", 'default text 1'),			//object has automatic id, name is set to 'txt1' by controller
	array('txt2', "input(\$id)->type('text')->value('default text 2')"),	//object has id. Default value is set regardless of control value, null here
	array('txt3', "input(\$id)->type('text')->auto_id()", 'default text 3'),	//object has id, and it's propagated to html element. Default value is overwritten by values_from_* methods
	array('check1', "input()->type('checkbox')->value(\$id)"),
	array('check2', "input()->type('checkbox')->value(\$id)", 'check2'),
	array('radio', null, 'rad1'),
	array('rad1', "input()->type('radio')->value(\$id)->name('radio')"),
	array('rad2', "input()->type('radio')->value(\$id)->name('radio')"),
	array('ta1', "textarea(null, \$id)->auto_id()", 'default text area'),		//beware, textarea in not an empty element so the id, if needed, must be the second parameter
	array('sel1', "select()
		->add(\$hop->option('option1')->value('op1'))
		->add(\$hop->option('option2')->value('op2'))
		->add(\$hop->option('option3')->value('op3'))", 'op2'),
	array('ok', "input()->type('submit')->value('Send')")			//declaration of button in controller has not utility beside grouping of controls
	) ) ;

$fc->values_from_request('ok');			// get values if submitted
$fc->values_from_data(array('radio'=>'rad2'));	//set values from array (object is ok too and less memory consuming)

$ul = $fc->hop_all('li()', 'ul()');		// get form elements wrapped in a list
$fld = $fc->hop_all('p()', 'fieldset()');	// get form elements in a fieldset

$fc->txt3 = 'late modif';			// control value is modified after the hop object are generated
$fc->map_global();				//progagate the modif of every controls with id set
$fc->map($ul->content[0]->last(), 'txt3');	// set this control value to an arbitrary element, with no id, accessed through its "path"
		
Comments :

The controller constructor has only one parameter, a definition array. Elements of this array are arrays, one for each control (form element). Each has three parameters, the last two being optional :

Notice the special case of the radio group which needs a control property to store the group value, and the way the name attribute is set on radio elements to group them with the control property.

After an element has been instantiated, change to the value at the control level is not reflected to the element object, unless you do it with one map method. In "normal" MVC flow, this should not be necessary.

Methods :

See source file

hop_form_controller.php

<?php
/*
« Copyright 2007 Max Barel a_x@ac-mb.info »
Release : 0.4 ($Rev : 85 $)
$Date : 2009-05-14 22:00:13 +0200 (Jeu, 14 mai 2009) $

This file is part of HoP.

HoP is free software ; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation ; either version 2.1 of the License, or
(at your option) any later version.

HoP 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with HoP ; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

class Hop_form_controller
{
protected $controls = array() ;	//array of unstantiation functions
public $trigger = null ;			//value of trigger control

function __construct($definition = array()) {
	// definition array must be of the form : array(
	// 		array('id', "input(\$id)->set_type('text')", 'value'),
	// 		array('ok', "input(\$id)->set_type('submit')", 'Submit data')
	// 	)
	// the second parameter is the hop function to instantiate the hop_element $id will be substituted at intantiation time
	foreach ($definition as $c) {
		$this->add_control($c);
	}
}

public function add_control($cont_def = array()) {
	if ($cont_def) {
		$cont_def[] = null ; $cont_def[] = null ; // in case not all parameters have been specified
		$this->controls[$cont_def[0]] = $cont_def[1]; //$this->controls['id'] = "function"
		$this->{$cont_def[0]} = $cont_def[2]; // $this->control_id = control_value
	}
	return $this ;
}

public function remove_control($id) {
	//remove a control, given its id. This function must be used BEFORE the HoP elements are instantiated. It will NOT remove an already instantiated control.
	unset($this->controls[$id]);
	unset($this->$id);
	return $this ;
}

public function values_from_request($trigger , &$mode = null, $index = null) { //$_REQUEST, $_POST, $_GET
	if (!isset($mode)) $mode =& $_REQUEST ;
	$this->trigger = isset($mode[$trigger]);
	if ($this->trigger) {
		$this->trigger = $mode[$trigger];
		foreach ($this->controls as $id => $f) {
			// echo "$id\n";
			// attention l'index d'un control a deux utilisations : pour les formulaire multiples (utilisation du parametre $index) ou pour les select/radio/checkbox. par combinaison, ces derniers sont de second niveaux.
			$this->$id = ((isset($mode[$id]) and $mode[$id] != 'hop_null') ? (is_array($mode[$id]) && $index !== null ? $mode[$id][$index] : $mode[$id]) : null) ;
			// $this->$id = ((isset($mode[$id]) and $mode[$id] != 'hop_null') ? (is_array($mode[$id]) ? @$mode[$id][$index !== null ? $index : 0] : $mode[$id]) : null);
		}
	}
	return $this ;
}

// public function trigger() {
// 	return $this->trigger;
// }
// 
public function values_from_data($obj_or_array) {
	if (is_array($obj_or_array)) $obj_or_array = (object) $obj_or_array ;
	foreach ($this->controls as $id => $f) $this->$id = (isset($obj_or_array->$id) ? $obj_or_array->$id : null);
	return $this ;
}

public function values_to_data(& $obj_or_array) {
	foreach ($obj_or_array as $id => $val)
		if (isset($this->$id)) {
			if (is_array($obj_or_array)) $obj_or_array[$id] = $this->$id;
			elseif ((is_object($obj_or_array))) $obj_or_array->$id = $this->$id;
		}
	return $this ;
}

public function values($reg_exp = null, $def = null, $adslsh = false) {
	//returns on array of values, if no default value is given, only non null values are returned. Pass the empty string to get all values ;
	$values = array() ;
	foreach ($this->controls as $id => $f) {
		if ((is_null($reg_exp) or preg_match($reg_exp, $id))
			and (isset($def) or isset($this->$id))
			) {
				$values[$id] = (isset($this->$id) ? (is_array($this->$id) ? implode(',', $this->$id) : $this->$id) : $def);
				if ($adslsh) $values[$id] = addslashes($values[$id]) ;
			}
	}
	return $values ;
}

public function map($ho, $id) {
	// map the control value to the existing hop_element oject
	switch ($ho->type) {
		case 'input' :
			switch ($ho->attributes->type) {
				case 'checkbox' :
					$ho->remove_checked();
					// $groupe = str_replace('[]', '', $ho->get_name());
					$groupe = ereg_replace('[[0-9]*]', '', $ho->get_name());
					if (@$this->$groupe) {
						$values = (is_array($this->$groupe) ? $this->$groupe : explode(',', $this->$groupe));
						if (in_array($ho->get_value(), $values) ) $ho->set_checked('checked');
					} else
					if ($this->$id == $ho->get_value()) $ho->set_checked('checked');
					break ;
				case 'radio' :
					$groupe = ereg_replace('[[0-9]*]', '', $ho->get_name());
					if ($this->$groupe == $ho->get_value()) $ho->set_checked('checked');
					else $ho->remove_checked();
					// echo "$groupe : $id / {$this->$groupe} == {$ho->get_value()} = {$ho->get_checked()}\n";
					break ;
				case 'submit' :
				case 'reset' :
				case 'image' :
				case 'button' :
					// break ;
				default :
					if (isset($this->$id)) $ho->set_value($this->$id);
					break ;
			}
			break ;
		case 'select' :
			$values = (is_array($this->$id) ? $this->$id : explode(',', $this->$id));
			foreach($ho->content as $op) {
				if (in_array($op->get_value(), $values)) $op->set_selected('selected');
				else $op->remove_selected();
			}
			break ;
		case 'textarea' :
			$ho->set_content(htmlspecialchars($this->$id));
			break ;
		default :
			if (isset($this->$id)) $ho->set_value($this->$id);
			break ;
	}
	return $this ;
}

public function hop($id) {
	$hop = Hop::single() ; //get the global hop factory
	// instantiate and return the hop_element for this id
	if ($f = $this->controls[$id]) eval ("\$hop_el =  \$hop->$f;");
	else throw new Exception("No instantiation function definition for $id");
	if ( ! $hop_el instanceof Hop_element) return "Invalid Hop_element : $id"; // throw new Exception("Invalid element : $id");
	if (!$hop_el->get_name()) $hop_el->set_name($id);
	$this->map($hop_el, $id);
	return $hop_el ;
}

public function hop_all($wrapper = null, $f_container = null, $reg_exp = null) {
	$hop = Hop::single() ; //get the global hop factory
	if ($f_container) eval("\$container = \$hop->$f_container;");
	else $container = new Hop_node ;
	foreach ($this->controls as $id => $f) {
		if ($f and (is_null($reg_exp) or preg_match($reg_exp, $id))) {
			if ($wrapper) $container->add(eval("return \$hop->{$wrapper}->add(\$this->hop(\$id));"));
			else $container->add($this->hop($id));
		}
	}
	return $container ;
}

public function hop_range($wrapper = null, $f_container = null, $start = null, $end = null) {
	$hop = Hop::single() ; //get the global hop factory
	if ($f_container) eval("\$container = \$hop->$f_container;");
	else $container = new Hop_node ;
	$range = !($start and $end) ;
	foreach ($this->controls as $id => $f) {
		if (!$range and $id == $start) $range = !$range ;
		if ($f and $range) {
			if ($wrapper) $container->add(eval("return \$hop->{$wrapper}->add(\$this->hop(\$id));"));
			else $container->add($this->hop($id));
		}
		if ($range and $id == $end) $range = !$range ;
	}
	return $container ;
}

public function map_global() {
	foreach ($this->controls as $id => $f) {
		if (isset(Hop_element::$pool->$id)) $this->map(Hop_element::$pool->$id, $id);
	}
	return $this ;
}
}
/*
Example usage :
$fc = new Hop_form_controller( array(
array('txt1', "input()->set_type('text')", 'default text 1'), //object has automatic id, name is set to 'txt1' by controler
array('txt2', "input(\$id)->set_type('text')->set_value('default text 2')"), //object has id. Default value is set regardless of control value, null here
array('txt3', "input(\$id)->set_type('text')->auto_id()", 'default text 3'), //object has id, and it's propagated to html element. Default value is overwritten by values_from_* methods
array('check1', "input()->set_type('checkbox')->set_value(\$id)"),
array('check2', "input()->set_type('checkbox')->set_value(\$id)", 'check2'),
array('radio', null, 'rad1'),
array('rad1', "input()->set_type('radio')->set_value(\$id)->set_name('radio')"),
array('rad2', "input()->set_type('radio')->set_value(\$id)->set_name('radio')"),
array('ta1', "textarea(null, \$id)->auto_id()", 'default text area'), //beware, textarea in not an empty element so the id, if needed, must be the second parameter
array('sel1', "select()
	->add(\$hop->option('option1')->set_value('op1'))
	->add(\$hop->option('option2')->set_value('op2'))
	->add(\$hop->option('option3')->set_value('op3'))", 'op2'),
array('defered_select', "pool('model_select')"), //This select wil be linked at instantiation time to a prgram built model.
array('clone_select', "clone_el(\$hop->pool('model_select'))", 'atelier'), //This select wil be cloned at instantiation time from a prgram built model. This is useful if you need several element with same base content. Attributes are cloned too, so they can differ.
array('ok', "input()->set_type('submit')->set_value('Send')") //declaration of button in controler has not utility beside grouping of controls
) ) ;

$fc->values_from_request('ok'); // get values if submitted
$fc->values_from_data(array('radio'=>'rad2')); //set values from array (object is ok too and less memory consuming)

$ul = $fc->hop_all('li()', 'ul()'); // get form elements wrapped in a list
$fld = $fc->hop_all('p()', 'fieldset()'); // get form elements in a fieldset

$fc->txt3 = 'late modif'; // control value is modified after the hop object are generated
$fc->map_global(); //progagate the modif of every controls with id set
$fc->map($ul->content[0]->last(), 'txt3'); // set this control value to an arbitrary element, whith no id, accessed through its "path"

*/

?>

Hop_node_from_file

This special class is designed to simplify integration with html template files. See the relevant chapter for a general discussion on this topic.

See source file

hop_node_from_file.php

<?php
/*
« Copyright 2007 Max Barel a_x@ac-mb.info »
Release : 0.4 ($Rev : 79 $)
$Date : 2009-02-09 19:50:35 +0100 (Lun, 09 fév 2009) $

This file is part of HoP.

HoP is free software ; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation ; either version 2.1 of the License, or
(at your option) any later version.

HoP 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with HoP ; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

class Hop_node_from_file extends Hop_node
{
public $file ;

function __construct($file) {
	$this->file = $file;
	$text = file_get_contents($this->file);
	extract($GLOBALS) ; //for correct substitution of php variable inside the file
	eval ('$text =  "' . addcslashes($text, '"') . '";');
	// suppress global html to allow insertion of wysiwyg edited html as a chunk
	$text = preg_replace('/^.*< *body[^>]*>/is', '', $text); //from start to body tag
	$text = preg_replace('/< *\/body[^>]*>.*$/is', '', $text); //from /body to end
	$this->text = $text;
	// echo $this->text;
}

public function render($indent = '') {
	// $text = file_get_contents($this->file);
	$text = $this->text;
	$text = preg_replace(array(
			'/^\t?/m' //indent the chunk to the current level
			, '/[   ]*([;:?!»])(?=\s|<)/u' //pour fine insecable
			, '/([« ])[   ]*/u' //pour fine insecable
			, '/([0-9])\s([0-9])/u' //fine sur nombres
			, '/&(.*?);/u' // correction effet indésirable ci-dessus
		), array(
			$indent
			,' $1' // fine insecable entre crochets une fine insécable[ ]
			,'$1 ' // fine insecable &#8239;
			,'$1 $2'
			, '&$1;' //rétablissement entitées
		)
		, trim($text)) ;
	if ($this->base_img) $text = str_replace('src="img/', "src=\"{$this->base_img}img/", $text);
	// extract($GLOBALS) ; //for correct substitution of php variable inside the file
	// eval ('$text =  "' . addcslashes($text, '"') . '";');
	return "\n$text";
}
public static function static_render($file) {
	extract($GLOBALS) ;
	// eval ('return "' . addcslashes(file_get_contents($file), '"') . '";');
	//évolution pour expression avec ""
	eval ('$text = "' . addcslashes(preg_replace('/\{(.*)"([^"]*)"(.*)\}/U',"{\$1'\$2'\$3}", file_get_contents($file)), '"') . '";');
	return $text ;
	// eval ("return \"$text\"");
}
}

?>

Hop_node_from_buffer

A very simple class to help migrating to HoinP.

When you need to mix, in a view, parts generated from HoinP and parts from the "echo" side, for example a forum module or whatever, this is it.

$debug = new Hop_node_from_buffer ;
$debug->start_buffering();
echo 'I hate the echo method!' ;
$debug->stop_buffering();

By buffering the standard output, its content is captured and stored in this special node for subsequent rendering, at the right place in your view.

See source file

hop_node_from_buffer.php

<?php
/*
« Copyright 2007 Max Barel a_x@ac-mb.info »
Release : 0.4 ($Rev : 47 $)
$Date : 2007-09-09 15:43:54 +0200 (Dim, 09 sep 2007) $

This file is part of HoP.

HoP is free software ; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation ; either version 2.1 of the License, or
(at your option) any later version.

HoP 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with HoP ; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

class Hop_node_from_buffer extends Hop_node
{
function start_buffering() { ob_start() ; }

function stop_buffering() { $this->set_content(ob_get_clean()); }
}

?>

Widgets

Hop_page

A simple widget to build a base XHTML page. Source is self-explanatory. <html> and <meta /> elements are full HoP elements, this way you can change default settings.

//notice the use of factory method for direct fluid method chaining.
$page = Hop_page::factory('this is the title')->html->setAttr('xml:lang', $session->lang)->lang($session->lang);

A commodity trick is that head and body element are cross referenced from the page level for simpler access :

$page->head === $page->html->head; //true

See source file

hop_page.php

<?php
/*
« Copyright 2007 Max Barel a_x@ac-mb.info »
Release : 0.4 ($Rev : 67 $)
$Date : 2008-03-09 22:02:56 +0100 (Sun, 09 Mar 2008) $

This file is part of HoP.

HoP is free software ; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation ; either version 2.1 of the License, or
(at your option) any later version.

HoP 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with HoP ; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

class Hop_page extends Hop_node {
public $id ; //page id is needed to avoid conflict in the gobal pool when several pages are built, i.e. for html messages

function __construct($title_or_options = array()) {
	//options_array_or_object = array('title' => string, 'lang' => string, 'id' => string)
	
	//default options
	$title = 'Webmaster forgot to set this title!' ;
	$lang = 'en' ;
	$id = 'main' ;
	if (is_string($title_or_options)) $title = $title_or_options ;
	else extract((array) $title_or_options) ;
	 
	$hop = Hop::single() ; //get the global hop factory
	$this->doctype = $this->add('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . (Hop_element::$pack ? "\n" : ''))->last();
	$this->html = $hop->html("{$id}_html")->xmlns('http://www.w3.org/1999/xhtml')->set_attr('xml:lang', $lang)->lang($lang);
	$this->add($this->html);
	$this->head = $hop->head("{$id}_head");
	$this->html->add($this->head);
	$this->head->add($hop->meta("{$id}_content_type")->set_attr('http-equiv', 'Content-Type')->set_attr('content', 'text/html; charset=utf-8')) ;
	$this->title = $hop->title($title);
	$this->head->add($this->title);
	$this->body = $hop->body("{$id}_body");
	$this->html->add($this->body);
}

public static function factory($title = 'Webmaster forgot to set this title!') {
	return new self($title) ;
}

}

?>

Hop_menu

This widget build a list based, CSS popup menu. More about CSS and IE in the CSS menus page. It can automatically add a link element in the page header to reference the built-in CSS.

Methods :
Example :

This example shows the add_item() use. As it gives no page parameter the css will not be included automatically.

$menu = Hop_menu::factory()
	->add_item('first')
	->sub_menu(Hop_menu::factory()
		->add_item('sub_item_1_1')
		->add_item('sub_item_1_2')
		)
	->add_item('second')
	 ;

See below, the php code for this site menu. Note that using automatic translation in menu item text is much simpler if item content is hop generated than if raw text :

$menu = Hop_menu::factory(array(
	'id' => 'menu_g',
	'page' => $page,
	'css' => '../hop/widgets/hop_menu_IE6.css',
	'items' => array(
		'accueil' => $hop->a($tr->_("accueil"))->href('accueil.html'),
		'contributions' => $tr->_("contributions"),
		'web2' => $hop->a("Ajax, web 2")->href('web2.html'),
		// 'web2' => '<a href="web2.html">Ajax, web 2</a>',	//same as preceding with raw html content
		'xhtml' => $hop->a("XHTML")->href('xhtml.html'),
		'css' => $hop->a("CSS")->href('css.html'),
		'javascript' => $hop->a("JavaScript")->href('javascript.html'),
		'php' => $hop->a("PHP")->href('php.html'),
		'zend' => $hop->a("Zend framework")->href('zend.html'),
		'mvc' => $hop->a("MVC")->href('mvc.html'),
		'mysql' => $hop->a("MySQL")->href('mysql.html'),
		'i18n' => $hop->a("i18n")->href('i18n.html'),
		'spip' => $hop->a("CMS, SPIP")->href('spip.html')
		)))
	->sub_menu(Hop_menu::factory(array(
		'id'=>'logiciels',
		'items' => array(
			'hop' => $hop->a("Ho<sub>in</sub>P")->href('hop.html'),
			'menus' => $hop->a($tr->_("menus en CSS"))->href('menus.html')
			)))
	, 'contributions')
	->auto_id()
	 ;
$hop->pool()->$sujet->add_class('actif');	//pool access to reach submenu items
$page->head->hop_menu->auto_id();		//for javascript vivibility
		

See source file

hop_menu.php

<?php
/*
« Copyright 2007 Max Barel a_x@ac-mb.info »
Release : 0.4 ($Rev : 53 $)
$Date : 2007-10-05 14:58:16 +0200 (Ven, 05 oct 2007) $

This file is part of HoP.

HoP is free software ; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation ; either version 2.1 of the License, or
(at your option) any later version.

HoP 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with HoP ; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

class Hop_menu extends Hop_element
{
function __construct($options = array()) {
	// array('id' => 'menu_id', 'items' => array('item_id' => 'item_content'), 'page' => node, 'css' => filepath)
	$hop = Hop::single() ; //get the global hop factory
	//default settings
	$id = 'menu' ;
	$page = null ;
	$items = array() ;
	$css = 'hop/widgets/hop_menu.css' ;
	$class = 'hop_menu' ;
	extract ((array) $options) ; // get settings from options
	parent::__construct('ul', array('id'=>$id, 'nl'=>2));
	$this->set_class($class);
	foreach ($items as $it_id => $item) $this->add_item($item, $it_id);
	if ($css and $page and isset($page->head) and !isset($page->head->hop_menu))
		$page->head
			->insert($hop->link('hop_menu')->set_rel('stylesheet')->type('text/css')->set_media('screen')->set_href($css))
			->insert("\n\t\t" . '<!--[if lt IE 7]><style type="text/css">body {behavior : url("js/csshover.htc");}</style><![endif]-->') //[if lt IE 7]
		 ;
}

public static function factory($options = array()) {
	return new self($options) ;
}

public function sub_menu(self $m, $index = null) {
	if (isset($this->$index)) $this->$index->add($m)->add_class('has_sub_menu');
	elseif (isset($this->content[$index])) $this->content[$index]->add($m)->add_class('has_sub_menu');
	else $this->last()->add($m)->add_class('has_sub_menu');
	return $this ;
}
	
public function add_item($item = null, $options = null) {
	$hop = Hop::single() ; //get the global hop factory
	$this->add($hop->li($item, $options));
	return $this ;
}
}

?>

Integration with HTML files

In most pages, you'll need to mix markup from HoinP and raw markup, edited in you favorite editor, either WYSIWYG or not. According to your design, they will fall in one of two categories :

Main is HTML

Main means that the whole page is HTML an some part of it will come from HoP objects rendering. Here is a simple, partial example :

<body>
	<?php echo $hop_object->render() ?>
</body>

Not much to say, the usual include of such a file from your controller, if any, will do.

However, if there are many places in your file where you have objects to render, this syntax become cumbersome and, moreover, is not friendly to WYSIWYG editors.

In such cases you can use a static method from an HoP class : Hop_node_from_file::static_render(filename) in place of the include. It's much like an include, in fact an eval of the included file. The example above thus becomes :

<body>
	{$hop_object->render()}
</body>

This is much easier to type, WYSIWYG friendly and syntactically close to some other templating systems. Just keep in mind that eval'ing the file does not allow for PHP statements or expressions. Only variables substitutions. Nevertheless, object methods are OK, thus opening for rather powerful chained HoP expressions.

Main is HoinP

In this case, overall page structure is made from HoP objects, and you need to insert chunks of HTML markup when there is no benefit to use HoP, specially where editors do a better job.

The same Hop_node_from_file class does just this : return the html markup from a given file, at render time of the whole HoP tree.

If this file has been made with a WYSIWYG editor, it will have DOCTYPE, html, head and body elements which are not desired there. They are automatically stripped from the output at rendering time.

A typical case would be :

<?php

$page = new Hop_page('page title') ;
//add stuff

$page->body->add(new Hop_node_from_file('HTML_file_to_include'));

//add other stuff
echo $page->render();

?>

The code is eval'ed when rendered, as by static_render() method. Thus you can have pre-built HoP objects rendered in the markup. This page has numerous in-place rendering for source files display, while it is included as a big chunk in the general HoP structure of the site. Most code can be seen on the MVC page.

Final words

If there is enough interest on this library, I might set-up a forum. In the meantime, my email is on the home page.

Release notes

Valid XHTML 1.0 Strict