I found this class for threaded comment using php and MySQL:
<?php
class Threaded_comments
{
public $parents = array();
public $children = array();
/**
* #param array $comments
*/
function __construct($comments)
{
foreach ($comments as $comment)
{
if ($comment['parent_id'] === NULL)
{
$this->parents[$comment['id']][] = $comment;
}
else
{
$this->children[$comment['parent_id']][] = $comment;
}
}
}
/**
* #param array $comment
* #param int $depth
*/
private function format_comment($comment, $depth)
{
for ($depth; $depth > 0; $depth--)
{
echo "\t";
}
echo $comment['text'];
echo "\n";
}
/**
* #param array $comment
* #param int $depth
*/
private function print_parent($comment, $depth = 0)
{
foreach ($comment as $c)
{
$this->format_comment($c, $depth);
if (isset($this->children[$c['id']]))
{
$this->print_parent($this->children[$c['id']], $depth + 1);
}
}
}
public function print_comments()
{
foreach ($this->parents as $c)
{
$this->print_parent($c);
}
}
}
this worked with this array:
$comment = array( array('id'=>1, 'parent_id'=>NULL, 'text'=>'Parent'),
array('id'=>2, 'parent_id'=>1, 'text'=>'Child'),
array('id'=>3, 'parent_id'=>2, 'text'=>'Child Third level'),
array('id'=>4, 'parent_id'=>NULL, 'text'=>'Second Parent'),
array('id'=>5, 'parent_id'=>4, 'text'=>'Second Child')
);
and for result:
$tc = new Threaded_comments($comment);
$tc->print_comments();
and result is:
Parent
Child
Child Third level
Second Parent
Second Child
this worked true But for defign comment list(html) I need to add all html code into private function format_comment($comment, $depth). this is not good way and I think need separate html from php class.
EDIT:(this is my page for show/print threaded comment)
function _comments_($id,$type){
$DB_QUERY = mySqli::f("SELECT id,user,email,message,timestamp,parent_id,rfield_1,rfield_2,rfield_3,rfield_4,rfield_5 FROM " . NEWS_COMMENTS . " LEFT JOIN " . NEWS_REVIEWS . " ON " . NEWS_COMMENTS . ".id = " . NEWS_REVIEWS . ".cid WHERE
pid = ? AND type = ? AND approved = 1 ORDER BY timestamp DESC LIMIT 12", $id, $type);
foreach($DB_QUERY as $row){
$commentdata[] = $row;
}
return $commentdata;
}
$comments_list = _comments_('125','book');
foreach($comments_list as $row){
$comments[] = array(
'id' => $row['id'],
'parent_id' => $row['parent_id'],
'name' => $row['user'],
'text' => $row['message'],
'datetime' => $row['timestamp']
);
}
$tc = new Threaded_comments($comments);
$tc->print_comments();
In my page threaded comments show with format_comment and work true. in case i need to design output using separate html from php class.
how do we separate html code from class in this case?!
You're right.
1) Most simple solution is to pass your array to a template. This template doesn't contain logic, but only html and php (e.g. foreach of your your array).
2) For beter separation, use Model-View-Controller pattern:
With this layer construction, each layer has its own responsibilities. Never html in model and controller. Queries in the models only.
Frameworks as Laravel and CodeIgniter have an implementation of this pattern.
Related
Hard limit is working thanks to Luuk, but pagination is still not working.
Based on my previous question, I am currently building comments system for a blog and want to make it reddit like.
I am currently using the nested comments by http://www.jongales.com/blog/2009/01/27/php-class-for-threaded-comments/
class Threaded_comments
{
public $parents = array();
public $children = array();
/**
* #param array $comments
*/
function __construct($comments)
{
foreach ($comments as $comment)
{
if ($comment['parent_id'] === NULL)
{
$this->parents[$comment['id']][] = $comment;
}
else
{
$this->children[$comment['parent_id']][] = $comment;
}
}
}
/**
* #param array $comment
* #param int $depth
*/
private function format_comment($comment, $depth)
{
for ($depth; $depth > 0; $depth--)
{
echo "\t";
}
echo $comment['text'];
echo "\n";
}
/**
* #param array $comment
* #param int $depth
*/
private function print_parent($comment, $depth = 0)
{
foreach ($comment as $c)
{
$this->format_comment($c, $depth);
if (isset($this->children[$c['id']]))
{
$this->print_parent($this->children[$c['id']], $depth + 1);
}
}
}
public function print_comments()
{
foreach ($this->parents as $c)
{
$this->print_parent($c);
}
}
}
Here’s the example usage with the data provided as an array. Remember that if your data is in another format you’ll have to modify the class.
$comments = array( array('id'=>1, 'parent_id'=>NULL, 'text'=>'Parent'),
array('id'=>2, 'parent_id'=>1, 'text'=>'Child'),
array('id'=>3, 'parent_id'=>2, 'text'=>'Child Third level'),
array('id'=>4, 'parent_id'=>NULL, 'text'=>'Second Parent'),
array('id'=>5, 'parent_id'=>4, 'text'=>'Second Child')
);
$threaded_comments = new Threaded_comments($comments);
$threaded_comments->print_comments();
Example Output:
Parent
Child
Child Third level
Second Parent
Second Child
I have set the depth limit, created the html output and everything, but the problem is pagination. Whenever I try to do a pagination via offset limit, it disrupts the child comments and the structure because the next page does not have original parent where to attach children and such.
For example:
select count(*) as found_comments from comments where blog_post = 1 and parent_id is null;
result = 20; // lets say its 20 parent comments limit from query above.
$query = "select * from comments where blog_post = 1 LIMIT 0,20";
$comments = [];
foreach ($query as $row)
{
$comments[] = [
'id' => $row['id'],
'parent_id' => $row['parent_id'],
'text' = $row['text'],
'added' => $row['added']];
}
$threaded_comments = new Threaded_comments($comments);
$threaded_comments->print_comments();
This code will limit both parent and children comments, but I need to set it to limit only parents, so 20 parents per page.
If I set in count query WHERE blog_post = 1 AND parent_id = 0
it will count parents only, but will cut out children comments when 20 quota is reached in $comments query.
It's really headache to find a way to do this. Any help is appreciated.
An example of how to change your code to only print 2 parents:
public function print_comments($count =2)
{
foreach ($this->parents as $c)
{
$this->print_parent($c);
$count--;
if($count == 0) exit;
}
}
I can get the not-bind query on with this way :
\DB::enableQueryLog();
$items = OrderItem::where('name', '=', 'test')->get();
$log = \DB::getQueryLog();
print_r($log);
Output is :
(
[0] => Array
(
[query] => select * from "order_items" where "order_items"."name" = ? and "order_items"."deleted_at" is null
[bindings] => Array
(
[0] => test
)
[time] => 0.07
)
)
But what I really need is bind query like this :
select * from "order_items" where "order_items"."name" = 'test' and "order_items"."deleted_at" is null
I know I can do this with raw PHP but is there any solution in laravel core?
Actually I've created one function within helpers.php for same. You can also use same function within your helpers.php file
if (! function_exists('ql'))
{
/**
* Get Query Log
*
* #return array of queries
*/
function ql()
{
$log = \DB::getQueryLog();
$pdo = \DB::connection()->getPdo();
foreach ($log as &$l)
{
$bindings = $l['bindings'];
if (!empty($bindings))
{
foreach ($bindings as $key => $binding)
{
// This regex matches placeholders only, not the question marks,
// nested in quotes, while we iterate through the bindings
// and substitute placeholders by suitable values.
$regex = is_numeric($key)
? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"
: "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";
$l['query'] = preg_replace($regex, $pdo->quote($binding), $l['query'], 1);
}
}
}
return $log;
}
}
if (! function_exists('qldd'))
{
/**
* Get Query Log then Dump and Die
*
* #return array of queries
*/
function qldd()
{
dd(ql());
}
}
if (! function_exists('qld'))
{
/**
* Get Query Log then Dump
*
* #return array of queries
*/
function qld()
{
dump(ql());
}
}
Simply place these three functions within your helpers.php file and you can use same as follows:
$items = OrderItem::where('name', '=', 'test')->get();
qldd(); //for dump and die
or you can use
qld(); // for dump only
Here I extended the answer of #blaz
In app\Providers\AppServiceProvider.php
Add this on boot() method
if (env('APP_DEBUG')) {
DB::listen(function($query) {
File::append(
storage_path('/logs/query.log'),
self::queryLog($query->sql, $query->bindings) . "\n\n"
);
});
}
and also added a private method
private function queryLog($sql, $binds)
{
$result = "";
$sql_chunks = explode('?', $sql);
foreach ($sql_chunks as $key => $sql_chunk) {
if (isset($binds[$key])) {
$result .= $sql_chunk . '"' . $binds[$key] . '"';
}
}
$result .= $sql_chunks[count($sql_chunks) -1];
return $result;
}
Yeah, you're right :/
This is a highly requested feature, and i have no idea why its not a part of the framework yet...
This is not the most elegant solution, but you can do something like this:
function getPureSql($sql, $binds) {
$result = "";
$sql_chunks = explode('?', $sql);
foreach ($sql_chunks as $key => $sql_chunk) {
if (isset($binds[$key])) {
$result .= $sql_chunk . '"' . $binds[$key] . '"';
}
}
return $result;
}
$query = OrderItem::where('name', '=', 'test');
$pure_sql_query = getPureSql($query->toSql(), $query->getBindings());
// Or like this:
$data = OrderItem::where('name', '=', 'test')->get();
$log = DB::getQueryLog();
$log = end($log);
$pure_sql_query = getPureSql($log['query'], $log['bindings']);
You can do that with:
OrderItem::where('name', '=', 'test')->toSql();
I'm trying to get a complete list of all models under app/Model.
Already tried App::objects('Model') but it only retrieves loaded models.
Is this possible in CakePHP 2?
After some research I found that App::objects('Model') returns all models under app/Models but it doesn't include Plugin models.
To include all models (app models and plugin models) I created the next function:
/**
* Get models list
*
* #return array
*/
public static function getModels()
{
// Get app models
$models = App::objects('Model');
$models = array_combine($models, $models);
// Get plugins models
$pluginsFolder = new Folder(APP . 'Plugin');
$plugins = $pluginsFolder->read();
foreach ( $plugins[0] as $plugin ) {
$pluginModels = App::objects($plugin . '.Model');
foreach ($pluginModels as $pluginModel) {
$models[$plugin . '.' . $pluginModel] = $plugin . '.' . $pluginModel;
}
}
// Exclude tableless and application models
$dataSource = ConnectionManager::getDataSource('default');
$sources = $dataSource->listSources();
foreach($models as $key => $model) {
$table = Inflector::tableize(self::modelName($key));
if (stripos($model, 'AppModel') > -1 || !in_array($table, $sources)) {
unset($models[$key]);
}
}
return $models;
}
Maybe late but here is my version:
(loops through plugin models, and retrieves the table associated by opening the file, and searching for the variable $useTable)
/**
* Get models list.
* Retrieved from: https://stackoverflow.com/questions/38622473/get-models-list-in-cakephp-2
* #return array
*/
public static function getModels() {
// Get app models
$models = App::objects('Model');
$models = array_combine($models, $models);
// Get plugins models
$pluginsFolder = new Folder(APP . 'Plugin');
$plugins = $pluginsFolder->read();
foreach ( $plugins[0] as $plugin ) {
$pluginModels = App::objects($plugin . '.Model');
foreach ($pluginModels as $pluginModel) {
$fullPath = APP.'Plugin'.DS.$plugin.DS."Model".DS.$pluginModel.".php";
$models[$plugin . '.' . $pluginModel] = $fullPath;
}
}
foreach($models as $model => $modelPath) {
if(file_exists($modelPath)) {
$data = file_get_contents($modelPath);
$find = array();
$find[] = "useTable = '";
$find[] = "useTable='";
$find[] = "useTable= '";
$find[] = "useTable ='";
foreach($find as $condition) {
$pos = strrpos($data, $condition);
if($pos !== false ) {
$pos+=(strlen($condition));
$tableName = substr($data, $pos, (strpos($data, "';", $pos)-$pos));
debug($tableName);
}
}
}
}
}
CakePHP v2
In AppController
if (in_array('YOUR_MODEL', $this->uses)) {
//found
}
I'm trying to display this kind of array:
$nodes = array(
1 => array(
'title' => 'NodeLvl1',
'children' => array(),
),
2 => array(
'title' => 'NodeLvl1',
'children' => array(
1 => array(
'title' => 'NodeLvl2',
'children' => array(),
),
2 => array(
'title' => 'NodeLvl2',
'children' => array(
1 => array(
'title' => 'NodeLvl3',
'children' => array(),
),
2 => array(
'title' => 'NodeLvl3',
'children' => array(),
),
),
),
),
),
3 => array(
'title' => 'NodeLvl1',
'children' => array(),
),
);
like this:
<ul>
<li>
NodeLvl1
</li>
<li>
NodeLvl1
<ul>
<li>NodeLv2</li>
...
</ul>
</li>
...
Basically a nested list taking into account the "children" property. So far I've come up with this:
class It extends RecursiveIteratorIterator{
protected
$tab = "\t";
public function beginChildren(){
if(count($this->getInnerIterator()) == 0)
return;
echo str_repeat($this->tab, $this->getDepth())."<ul>\n";
}
public function endChildren(){
if(count($this->getInnerIterator()) == 0)
return;
echo str_repeat($this->tab, $this->getDepth())."\n</ul>";
}
public function nextElement(){
echo str_repeat($this->tab, $this->getDepth() + 1).'<li>';
}
}
$it = new It(new RecursiveArrayIterator($nodes));
foreach($it as $key => $item)
echo $item;
Which doesn't work quite right: I get each item wrapped between <ul>s and I don't know how can I close <li>s...
Any ideas on how to make this work? Also is it possible to get all the array properties (the actual element), instead of just the "title" property inside my foreach() loop? And can this be done with objects instead of arrays?
Do you need a class iterator for this? You could do this with just a simple function...
function arrayToListHTML($array, $level = 0) {
static $tab = "\t";
if (empty($array)) return;
$tabs = str_repeat($tab, $level * 2);
$result = "{$tabs}<ul>\n";
foreach ($array as $i => $node):
$result .= "{$tabs}{$tab}<li>\n{$tabs}{$tab}{$tab}{$node['title']}\n".arrayToListHTML($node['children'], $level + 1)."{$tabs}{$tab}</li>\n";
endforeach;
$result .= "{$tabs}</ul>\n";
return $result;
}
Which will produce this output:
<ul>
<li>
NodeLvl1
</li>
<li>
NodeLvl1
<ul>
<li>
NodeLvl2
</li>
<li>
NodeLvl2
<ul>
<li>
NodeLvl3
</li>
<li>
NodeLvl3
</li>
</ul>
</li>
</ul>
</li>
<li>
NodeLvl1
</li>
</ul>
This covers what you've shown us, but I'm not sure what you mean by other properties. Are there more properties in each array other than title and children?
Instead of trying to use your class like an array in foreach() consider using your class to perform the function. For instance, the following code will output correctly but the function is performed inside the class.
class It extends RecursiveIteratorIterator{
protected
$tab = "\t";
public function beginChildren(){
if(count($this->getInnerIterator()) == 0)
return;
echo str_repeat($this->tab, $this->getDepth())."<ul>\n";
}
public function endChildren(){
if(count($this->getInnerIterator()) == 0)
return;
echo str_repeat($this->tab, $this->getDepth)."\n</ul>";
}
public function nextElement(){
echo str_repeat($this->tab, $this->getDepth())."<li>".$this->current()."</li>\n";
}
}
$it = new It(new RecursiveArrayIterator($nodes));
foreach($it as $key => $item)
//echo $item;
//it will be better to write a function inside your custom iterator class to handle iterations
?>
You can use RecursiveCachingIterator to do what you want. Here is an example, (source: https://github.com/cballou/PHP-SPL-Iterator-Interface-Examples/blob/master/recursive-caching-iterator.php)
<?php
// example navigation array
$nav = array(
'Home' => '/home',
'Fake' => array(
'Double Fake' => array(
'Nested Double Fake' => '/fake/double/nested',
'Doubly Nested Double Fake' => '/fake/double/doubly'
),
'Triple Fake' => '/fake/tripe'
),
'Products' => array(
'Product 1' => '/products/1',
'Product 2' => '/products/2',
'Product 3' => '/products/3',
'Nested Product' => array(
'Nested 1' => '/products/nested/1',
'Nested 2' => '/products/nested/2'
)
),
'Company' => '/company',
'Privacy Policy' => '/privacy-policy'
);
class NavBuilder extends RecursiveIteratorIterator {
// stores the previous depth
private $_depth = 0;
// stores the current iteration's depth
private $_curDepth = 0;
// store the iterator
protected $_it;
/**
* Constructor.
*
* #access public
* #param Traversable $it
* #param int $mode
* #param int $flags
*/
public function __construct(Traversable $it, $mode = RecursiveIteratorIterator::SELF_FIRST, $flags = 0)
{
parent::__construct($it, $mode, $flags);
// store the caching iterator
$this->_it = $it;
}
/**
* Override the return values.
*
* #access public
*/
public function current()
{
// the return output string
$output = '';
// set the current depth
$this->_curDepth = parent::getDepth();
// store the difference in depths
$diff = abs($this->_curDepth - $this->_depth);
// get the name and url of the nav item
$name = parent::key();
$url = parent::current();
// close previous nested levels
if ($this->_curDepth < $this->_depth) {
$output .= str_repeat('</ul></li>', $diff);
}
// check if we have the last nav item
if ($this->hasNext()) {
$output .= '<li>' . $name . '';
} else {
$output .= '<li class="last">' . $name . '';
}
// either add a subnav or close the list item
if ($this->hasChildren()) {
$output .= '<ul>';
} else {
$output .= '</li>';
}
// cache the depth
$this->_depth = $this->_curDepth;
// return the output ( we could've also overridden current())
return $output;
}
}
?>
Usage
<?php
try {
// generate the recursive caching iterator
$it = new RecursiveCachingIterator(new RecursiveArrayIterator($nav));
// build the navigation with the iterator
$it = new NavBuilder($it, RecursiveIteratorIterator::SELF_FIRST);
// display the resulting navigation
echo '<ul id="nav">' . PHP_EOL;
foreach ($it as $value) {
echo $value . "\n";
}
echo '</ul>' . PHP_EOL;
} catch (Exception $e) {
var_dump($e); die;
}
?>
First let me explain few things to you. Your array has two pattens
One with numeric indexes
One with string indexes, with title and children which has be parsed differently
I think a recursive function plays very nice role on this part, rather than complex logics. And our recursive function has to be able to handle both patterns separately.
Here is my version of the function you could use with explanation
function arraytolist(Array $array) { //ensure what you receive is array
if(count($array)) { //only if it has some items
//In case the array has `title` index we encountered out PATTERN 2
if(isset($array['title'])) {
$o = "<li>";
$o .= $array['title']; //simply add the title
$o .= arraytolist($array['children']); //and pass the children to this function to verify again
$o .= "</li>";
} else { //if its a normal array, //PATTERN 1
$o = "<ul>";
foreach($array as $value) {
$n = "";
if(is_array($value)) { //in case its an array again,
//send it to this very same function so that it will return as output again
$n .= arraytolist($value);
} else {
$n .= "<li>$value</li>";
}
$o .= strlen($n) ? $n : ""; //if $n has something use it otherwise not
}
$o .= "</ul>"; //lets close the ul
}
return $o;
}
}
Some Advantage of this function
No iteration level
As long as its an array and has item, keeps on building them
Power of simple logic in PHP
I would opt for a simple recursive function that flattens the array into the text/html format:
function arrToList( $arr, $embedded = false ) {
$output = array();
if ( $embedded ) $output[] = '<li>';
$output[] = '<ul>';
foreach ( $arr as $key => $values ) {
$output[] = '<li>'.$values['title'].'</li>';
if ( $values['children'] ) {
$output[] = arrToList( $values['children'], true );
}
}
$output[] = '</ul>';
if ( $embedded ) $output[] = '</li>';
return implode(PHP_EOL, $output);
}
Output from using your input:
NodeLvl1
NodeLvl1
NodeLvl2
NodeLvl2
NodeLvl3
NodeLvl3
NodeLvl1
or the actual code:
<ul>
<li>NodeLvl1</li>
<li>NodeLvl1</li>
<li>
<ul>
<li>NodeLvl2</li>
<li>NodeLvl2</li>
<li>
<ul>
<li>NodeLvl3</li>
<li>NodeLvl3</li>
</ul>
</li>
</ul>
</li>
<li>NodeLvl1</li>
</ul>
Cheers
I want know how to add custom attribute for option in a select field of Zend form.
PHP:
$option_values = array("multiOptions" => array(
"US" => "United States",
"CA" => "Canada",
));
$type=array('big','small');
$option= new Zend_Form_Element_Select('option', $option_values);
HTML:
<select>
<option value='US' type='big'>United States</option>
<option value='CA' type='small'>Canada</option>
</select>
How to add this type attribute in the option?
It is not possible using ZF's implementation of Zend_Form_Element_Select. You need to create your own element. I have done something similar, here's the relevant code:
<?php
require_once 'Zend/Form/Element/Select.php';
/**
* Select, but with the possibility to add attributes to <option>s
* #author Dominik Marczuk
*/
class Zend_Form_Element_SelectAttribs extends Zend_Form_Element {
public $options = array();
public $helper = 'selectAttribs';
/**
* Adds a new <option>
* #param string $value value (key) used internally
* #param string $label label that is shown to the user
* #param array $attribs additional attributes
*/
public function addOption ($value,$label = '',$attribs = array()) {
$value = (string) $value;
if (!empty($label)) $label = (string) $label;
else $label = $value;
$this->options[$value] = array(
'value' => $value,
'label' => $label
) + $attribs;
return $this;
}
}
Put this into /library/Zend/Form/Element/SelectAttribs.php. You also need a helper to render the element. Put it into your view helpers directory, name it SelectAttribs.php as well. Here's the contents of my file:
<?php
require_once 'Zend/View/Helper/FormElement.php';
class Zend_View_Helper_SelectAttribs extends Zend_View_Helper_FormElement {
public function selectAttribs($name, $value = null, $attribs = null, $options = null, $listsep = "<br />\n") {
$info = $this->_getInfo($name, $value, $attribs, $options, $listsep);
extract($info); // name, id, value, attribs, options, listsep, disable
// force $value to array so we can compare multiple values to multiple
// options; also ensure it's a string for comparison purposes.
$value = array_map('strval', (array) $value);
// now start building the XHTML.
$disabled = '';
if (true === $disable) {
$disabled = ' disabled="disabled"';
}
// Build the surrounding select element first.
$xhtml = '<select'
. ' name="' . $this->view->escape($name) . '"'
. ' id="' . $this->view->escape($id) . '"'
. $disabled
. $this->_htmlAttribs($attribs)
. ">\n ";
// build the list of options
$list = array();
$translator = $this->getTranslator();
foreach ($options as $opt_value => $option) {
$opt_disable = '';
if (is_array($disable) && in_array($opt_value, $disable)) {
$opt_disable = ' disabled="disabled"';
}
$list[] = $this->_build($option, $disabled);
}
// add the options to the xhtml and close the select
$xhtml .= implode("\n ", $list) . "\n</select>";
return $xhtml;
}
protected function _build($option, $disabled) {
$html = '<option';
foreach ($option as $attrib => $value) {
$html .= " $attrib=\"$value\"";
}
return $html.$disabled.">".$option['label']."</option>";
}
}
With this, you should be ready to go:
$elt = new Zend_Form_Element_SelectAttribs('whatever');
$elt->addOption($value,$label,array('attribname' => 'attribvalue'));
Using addMultiOption($value,$label) I just set the value parameter to something like:
$value = $id . '" ref="' . $ref;
and when it renders you get:
<option value="<idValue>" ref="<refValue"><labelValue></option>
Hope this helps....
Okay, value gets escaped but optionClasses does not so inside the loop that adds the addMultiOptions(val,lable) I do something like this:
$optionClasses[<val>] = 'ref_' . <val> . '" ref="' . <ref>;
and then after the loop just do a setAttrib('optionClasses',$optionClasses)
And that actually works...
I answered this for another question but could not find a way to add a comment here to reference it; It was Zend Framework addMultiOption adding custom parameters like "rel" for options
I didn't find #mingos's answer complete and had some issues with implementing setting the value. His answer helped a lot with what needed to be extended and changed however. Once I had that start the rest was pretty easy. I just extended ZF1's Select and overrode where I needed:
/**
* Select, now with abbility to specify attributes on <Option>, addMultiOption has new syntax
* #author Seth Miller
*/
class MyNamespace_Form_Element_SelectAttribs extends Zend_Form_Element_Select {
public $options = array();
public $helper = 'selectAttribs';
/**
* Add an option
*
* #param string $option
* #param string $value
* #return Zend_Form_Element_Multi
*/
public function addMultiOption($value, $label = '', $attribs = array()) {
$value = (string) $value;
if (!empty($label)) {
$label = (string) $label;
}
else {
$label = $value;
}
$this->_getMultiOptions();
if (!$this->_translateOption($value, $label)) {
$this->options[$value] = array(
'value' => $value,
'label' => $label
) + $attribs;
}
return $this;
}
/**
* Add many options at once
*
* #param array $options
* #return Zend_Form_Element_Multi
*/
public function addMultiOptions(array $options) {
foreach ($options as $optionKey => $optionProperties) {
if (is_array($optionProperties)
&& array_key_exists('key', $optionProperties)
&& array_key_exists('value', $optionProperties)
) {
if(array_key_exists('key', $optionProperties)) $optionKey = $optionProperties['key'];
$this->addMultiOption($optionKey, $optionProperties['value'], $optionProperties['attribs']);
}
else {
$this->addMultiOption($optionKey, $optionProperties);
}
}
return $this;
}
public function isValid($value, $context = null)
{
if ($this->registerInArrayValidator()) {
if (!$this->getValidator('InArray')) {
$multiOptions = $this->getMultiOptions();
$options = array();
foreach ($multiOptions as $optionKey => $optionData) {
// optgroup instead of option label
if (is_array($optionData['options'])) {
$options = array_merge($options, array_keys($optionData['options']));
}
else {
$options[] = $optionKey;
}
}
$this->addValidator(
'InArray',
true,
array($options)
);
}
}
return parent::isValid($value, $context);
}
}
And the view helper:
class MyNamespace_View_Helper_SelectAttribs extends Zend_View_Helper_FormElement {
public function selectAttribs($name, $value = null, $attribs = null, $options = null, $listsep = "<br />\n") {
$info = $this->_getInfo($name, $value, $attribs, $options, $listsep);
extract($info); // name, id, value, attribs, options, listsep, disable
// force $value to array so we can compare multiple values to multiple
// options; also ensure it's a string for comparison purposes.
$value = array_map('strval', (array) $value);
// check if element may have multiple values
$multiple = '';
if (substr($name, -2) == '[]') {
// multiple implied by the name
$multiple = ' multiple="multiple"';
}
if (isset($attribs['multiple'])) {
// Attribute set
if ($attribs['multiple']) {
// True attribute; set multiple attribute
$multiple = ' multiple="multiple"';
// Make sure name indicates multiple values are allowed
if (!empty($multiple) && (substr($name, -2) != '[]')) {
$name .= '[]';
}
}
else {
// False attribute; ensure attribute not set
$multiple = '';
}
unset($attribs['multiple']);
}
// now start building the XHTML.
$disabled = '';
if (true === $disable) {
$disabled = ' disabled="disabled"';
}
// Build the surrounding select element first.
$xhtml = '<select'
.' name="'.$this->view->escape($name).'"'
.' id="'.$this->view->escape($id).'"'
.$multiple
.$disabled
.$this->_htmlAttribs($attribs)
.">\n ";
// build the list of options
$list = array();
$translator = $this->getTranslator();
foreach ((array) $options as $optionKey => $optionData) {
if (isset($optionData['options'])) {
$optDisable = '';
if (is_array($disable) && in_array($optionData['value'], $disable)) {
$optDisable = ' disabled="disabled"';
}
if (null !== $translator) {
$optValue = $translator->translate($optionData['value']);
}
$optId = ' id="'.$this->view->escape($id).'-optgroup-'
.$this->view->escape($optionData['value']).'"';
$list[] = '<optgroup'
.$optDisable
.$optId
.' label="'.$this->view->escape($optionData['value']).'">';
foreach ($optionData['options'] as $optionKey2 => $optionData2) {
$list[] = $this->_build($optionKey2, $optionData2, $value, $disable);
}
$list[] = '</optgroup>';
}
else {
$list[] = $this->_build($optionKey, $optionData, $value, $disable);
}
}
// add the options to the xhtml and close the select
$xhtml .= implode("\n ", $list)."\n</select>";
return $xhtml;
}
/**
* Builds the actual <option> tag
*
* #param string $value Options Value
* #param string $label Options Label
* #param array $selected The option value(s) to mark as 'selected'
* #param array|bool $disable Whether the select is disabled, or individual options are
* #return string Option Tag XHTML
*/
protected function _build($optionKey, $optionData, $selected, $disable)
{
if (is_bool($disable)) {
$disable = array();
}
$opt = '<option';
foreach ($optionData as $attrib => $attribValue) {
$opt .= ' '.$this->view->escape($attrib).'="'.$this->view->escape($attribValue).'"';
}
// selected?
if (in_array((string) $optionData['value'], $selected)) {
$opt .= ' selected="selected"';
}
// disabled?
if (in_array($optionData['value'], $disable)) {
$opt .= ' disabled="disabled"';
}
$opt .= '>' . $this->view->escape($optionData['label']) . "</option>";
return $opt;
}
}
And implementation in a form would be something like:
$selectElement = new MyNamespace_Form_Element_SelectAttribs('selectElementName');
$selectElement->addMultiOption($value, $label, array('data-custom' => 'custom data embedded in option tag.');
I hope that helps someone. Thanks.
mingos,
you forgot about setvalue method of multiselect.
you shoul add something like:
...
foreach ($options as $opt_value => $option) {
$opt_disable = '';
$opt_selected = '';
if (is_array($disable) && in_array($opt_value, $disable)) {
$opt_disable = ' disabled="disabled"';
}
if (in_array($opt_value,$value)) {
$opt_selected = ' selected="selected"';
}
$list[] = $this->_build($option, $disabled, $opt_selected);
}
...
and
protected function _build($option, $disabled, $opt_selected) {
$html = '<option';
foreach ($option as $attrib => $value) {
$html .= " $attrib=\"$value\"";
}
return $html . $disabled . $opt_selected . " foo>" . $option['label'] . "</option>";
}
You could extend / overwrite the Zend_View_Helper_FormSelect helper but the real problem is going to be getting the extra data into each option.
By default, Zend_Form_Element_Select (via Zend_Form_Element_Multi) expects two strings for each option, one for the value attribute and an optional one for the text content. You may need to create your own element to handle the extra data.
No need for custom form element at all, what u can do is:
$element->setAttrib('disable', array(1, 2, 5));
As explained at http://pietervogelaar.nl/set-attribute-on-select-option-with-zend_form/
use Zend\Form\Element;
use Zend\Form\Form;
$select = new Element\Select('language');
$select->setLabel('Which is your mother tongue?');
$select->setValueOptions([
[
'value' => '0',
'label' => 'French',
'attributes' => [
'data-locale' => 'fr'
],
],
[
'value' => '1',
'label' => 'Italian',
'disabled' => true,
],
]);
$form = new Form('language');
$form->add($select);
I found this way much easier than any of the above suggestions