Still working on it, but here is the latest version. I changed a lot of the defaults, so if you used the previous version, please see the new comments.
I think the only major thing left to do is to allow a different order of looping. Right now you have to loop in "chunk1a==chunk2a==chunk3a||chunk1b==chunk2b==chunk3b", but some people may want to use "chunk1a==chunk1b||chunk2a==chunk2b||chunk3a==chunk3b". I also want to make the default == and || separators configurable.
<?php
/**
* Loop
*
* A looping snippet that loops through one or more arrays of placeholders to output several chunks at once (or separates with separators).
*
* @author Oleg Pryadko ([email protected])
* @copyright Copyright 2011, Oleg Pryadko
* @version 1 - Jan 30, 2011
* @license GPL (any version)
*
* To Do: allow on/ off switch of JSON-style vs. MODx-style arrays separate from the &ph placeholder
*
* PARAMETERS
*
* &tpl - Name of a chunk serving as a template for the loop
* [NOTE: if not provided, will use separators instead]
*
* &loop - JSON array of values to loop through (with or without placeholders). If loop is left blank, defaults to numbers.
*
* &numbers - (boolean). Adds numbers as placeholder values (if loop is set, adds them in addition to existing placeholders).
* &min - (if using numbers) Number to start with. Default is 0. Can be negative.
* &max - (if using numbers) Number to end with. Default is 3. Can be negative.
* &step - (if using numbers) Number to add with every iteration. Default is 1. Can be negative.
*
* &ph (or placeholder) - (optional) single placeholder or comma-separated list of placeholders
* [IMPORTANT: must be in the same order as &loop statement. If using numbers, put the numbers placeholder last.]
* &phSimple - &ph alternative - use MODx-style separation in &loop option (option==value||another option==another value)
*
* &prefix - Prefix to use before numbers if no placeholder is set. Defaults to "loop_" (e.g. [[+loop_1]], [[+loop_2]], etc...)
*
* &static - Static placeholders that are the same in every loop.
* If phSimple is active, use MODx-style arrays (&static=`lastname==Simpson||color==yellow`).
* Otherwise, use JSON-style array (&static=`{"lastname":"Simpson","color":"yellow"}`)
*
* &separator - Separator to put between chunks. If no template (&tpl) is provided, separate content with this (default is "\n")
* &separatorInner - If no template (&tpl) is provided, separate multiple placeholders with this (default is " ")
* &idx - Placeholder to use for idx. Setting to 0 will disable. (default is "idx")
*
* &toPlaceholder - Set placeholder
* &default - If for whatever reason the result is empty, return this.
* &debug - Print_r debug arrays
*
*
* PLACEHOLDERS
*
* Loop adds an [[+idx]] placeholder, which holds the iteration number (starting from 1).
* Use idx with the ":mod" output filter to add odd/ even logic to your templates.
*
* When no placeholders (or not enough placeholders) are provided, Loop
* makes its own placeholders by appending numbers to the prefix (starting from 0).
* For example, if your prefix is "loop_" (the default prefix), you will get
* [[+loop_0]], [[+loop_1]], [[+loop_2]], etc....
*
*
*
* EXAMPLES
*
* Using &loop WITHOUT &ph (default MODx-style arrays):
* - one placeholder only: [[Loop? &tpl=`loopTpl` &loop=`fistname==Homer==Marge==Lisa==Bart}`]]
* - multiple placeholders: [[Loop? &tpl=`loopTpl` &loop=`firstname==Homer==Marge==Lisa==Bart||lastname==Simpson==Simpson==Simpson==Simpson||phone==1-555-123-4567==1-555-123-4567===="`]]
* - using auto placeholders (results in [[+loop_0]], [[+loop_1]] and [[+loop_2]] placeholders): [[Loop? &tpl=`loopTpl` &loop=`==Homer==Marge==Lisa==Bart||==Simpson==Simpson==Simpson==Simpson||==1-555-123-4567==1-555-123-4567===="`]]
*
* NOTE: if you don't specify a template with &tpl, Loop will DISABLE the first value as placeholder even if you don't specify &ph
* So [[Loop? &loop=`==2==1` &separator=`=` &separatorInner=`+`]] will result in 1+=2+2=3+1, not 1+1=2+1 as normally expected without &ph
* (If you are wondering where the other numbers are coming from, it is from the IDX. Use &idx=`0` to disable)
*
* Using &loop WITH &ph=`` (default MODx-style arrays):
* - one placeholder: [[Loop? &tpl=`loopTpl` &ph=`firstname` &loop=`John==Joe==Lisa==Bart`]]
* - multiple placeholders:
* [[Loop? &tpl=`loopTpl` &ph=`firstname,lastname,phone` &loop=`John==Joe==Lisa==Bart||Simpson==Simpson==Simpson==Simpson||1-555-123-4567==1-555-123-4567`]]
* Note: the last two phone numbers are automatically filled in with an empty string
*
* JSON: Using &loop WITHOUT &ph AND &json=`1` (the alternative JSON-style arrays):
* - one placeholder only: [[Loop? &tpl=`loopTpl` &loop=`{"fistname":"Homer,Marge,Lisa,Bart"}`]]
* - multiple placeholders: [[Loop? &tpl=`loopTpl` &loop=`{"firstname":"Homer,Marge,Lisa,Bart","lastname":"Simpson,Simpson,Simpson,Simpson","phone":"1-555-123-4567,1-555-123-4567,,"}`]]
* - one automatic placeholder ([[+loop_0]]): [[Loop? &tpl=`loopTpl` &loop=`Homer,Marge,Lisa,Bart`]]
* - custom prefix with multiple automatic placeholders ([[+customprefix_0]], [[+customprefix_1]] and [[+customprefix_2]] ): [[Loop? &tpl=`loopTpl` &prefix=`customprefix` &loop=`{"0":"Homer,Marge,Lisa,Bart","1":"Simpson,Simpson,Simpson,Simpson","2":"1-555-123-4567,1-555-123-4567,,"}`]]
* - more complicated JSON: [[Loop? &tpl=`loopTpl` &name=`{"name":{"0":"Simpson, Homer","1":"Simpson, Marge","2":"Simpson, Lisa","3":"Simpson, Bart"},"phone":"1-555-123-4567,1-555-123-4567,,"}`]]
*
* JSON: Using &loop WITH &ph AND &json=`1` (the alternative JSON-style arrays):
* - one placeholder only: [[Loop? &tpl=`loopTpl` &json=`1` &ph=`firstname` &loop=`Sue,Joe,Mary,Bo`]]
* - multiple placeholders (JSON seems to need numbers starting from zero to signify multiple arrays):
* [[Loop? &tpl=`loopTpl` &json=`1` &ph=`firstname,lastname` &loop=`{"0":"Sue,Mary,Bilbo,Gandalf","1":"Smith,Brown,Baggins,Grey"}`]]
*
* Example using &numbers: [[!Loop? &tpl=`loopTpl` &ph=`name,number` &numbers=`1` &min=`3` &max=`0` &step=`-1` &loop=`Mary==Joe==Brian` &separator=`<br />`]]
*
*
* USES
*
* Great for generating complex FormIt, Register, and other custom forms with multiple option, text, or other fields.
* Call cached in the formit report to automatically output a list of form fields: [[Loop? &tpl=`formitReportRow` &ph=`name` &loop=`name==email==phone==message`]]
* The formitReportRow chunk: <div>[[+name:ucwords]]: <strong>[[+[[+name]]]]</strong></div>
*
* It's also useful for just about any other simple repetition need where using a custom snippet is overkill.
* Can easily be combined with other snippets (put them in the template chunks or use them to generate parameters).
*
*/
require_once ($modx->getOption('core_path').'components/loop/functions.inc.php');
$numbers = (bool)$modx->getOption('numbers',$scriptProperties,0);
$min = $modx->getOption('min',$scriptProperties,0);
$max = $modx->getOption('max',$scriptProperties,3);
$step = $modx->getOption('step',$scriptProperties,1);
$loop = $modx->getOption('loop',$scriptProperties,'');
$static = $modx->getOption('static',$scriptProperties,'');
if (empty($loop)) {$numbers = 1;}
/* set placeholder options */
$ph = $modx->getOption('ph',$scriptProperties,'');
if (empty($ph)) {$ph = $modx->getOption('placeholder',$scriptProperties,'');}
if (empty($ph)) {$ph = $modx->getOption('placeholders',$scriptProperties,'');}
if (empty($ph)) {$ph = $modx->getOption('phSimple',$scriptProperties,'');}
/* set whether to enable JSON (instead of MODx) style arrays */
$json = $modx->getOption('json',$scriptProperties,0);
if ($json) {$phSimple = 0;} else {$phSimple = 1;}
/* set templating options */
$tpl = $modx->getOption('tpl',$scriptProperties,'');
if (empty($tpl)) {$tpl = $modx->getOption('template',$scriptProperties,'');}
$separator = $modx->getOption('separator',$scriptProperties,"\n");
if (!$tpl) {$separatorInner = $modx->getOption('separatorInner',$scriptProperties," ");}
if (!$separatorInner) {$separatorInner = '';}
$toPlaceholder = $modx->getOption('toPlaceholder',$scriptProperties,false);
/* set miscellaneous options */
$prefix = $modx->getOption('prefix',$scriptProperties,'loop_');
$debug = $modx->getOption('debug',$scriptProperties,0);
$default = $modx->getOption('default',$scriptProperties,'');
$useIdx = $modx->getOption('idx',$scriptProperties,'idx');
$trim = $modx->getOption('trim',$scriptProperties,1);
$output = '';
if ($numbers) {
$numbersArray = array();
$numbersArray[0] = array();
$numDif = $max-$min;
if($numDif>0 && $step>0) {
for($i=$min;$i<$max;$i=$i+$step) {$numbersArray[0][] = $i;}
} elseif($numDif<0 && $step<0) {
for($i=$min;$i>$max;$i=$i+$step) {$numbersArray[0][] = $i;}
} else {$modx->log(modX::LOG_LEVEL_ERROR,'[Loop] Numbers function is not properly formed. Check min, max, and step parameters.'); $numbers = 0;}
if ($numbers && $loop) {$mergeNumbers = 1;}
elseif ($numbers && !$loop) {$loop = $modx->toJSON($numbersArray);}
}
if (!$numbers && !$loop) {$modx->log(modX::LOG_LEVEL_ERROR,'[Loop] Neither numbers nor loop is set. Nothing to do!'); return $default;}
/* make &loop into a basic array */
if (!$phSimple) {
$loopTempArray = $modx->fromJSON($loop);
if (!is_array($loopTempArray)) {
$loopTempArray = array(0 => explodeTrim(',',$loop,$trim));
}
} elseif ($phSimple && empty($ph) && !empty($tpl)) {
/* phSimple && empty($ph) - format is &loop=`placeholder==value==value||placeholder2==value==value` */
$loopArrayOuter = explodeTrim('||',$loop,$trim); /* 0 ==> ph==value==value */
$loopTempArray = array();
foreach ($loopArrayOuter as $key => $value) {
$tempHolderArray = explodeTrim('==',$value,$trim); /* 0 ==> placeholder, 1 ==> value, 2==> value */
$tempHolder = array_shift($tempHolderArray); /* tempHolder = placeholder, tempHolderArray = 0 ==> value, 1==> value */
$loopTempArray[$tempHolder] = $tempHolderArray; /* $loopTempArray["placeholder"] = array(0 ==> "value", 1==> "value"); */
}
unset($loopArrayOuter,$tempHolder,$tempHolderArray);
} else {
/* phSimple && !empty($ph) OR empty($tpl) - format is &loop=`value==value||value==value` */
$loopArrayOuter = explodeTrim('||',$loop,$trim);
$loopTempArray = array();
foreach ($loopArrayOuter as $key => $value) {
$loopTempArray[$key] = explodeTrim('==',$value,$trim); /* $loopTempArray[] = array(0 ==> "value", 1==> "value"); */
}
unset($loopArrayOuter);
}
if ($mergeNumbers) {$loopTempArray = array_merge($loopTempArray,$numbersArray);}
if ($debug) {$output .= 'loopTempArray (one): '.print_r($loopTempArray,1)."<br />\n";}
/* make array in the format of placeholder => valuesToLoopThrough */
if (!empty($ph)) {
/* placeholder in a separate array. Compbine placeholders with loopArray */
$placeholderArray = explodeTrim(',',$ph,$trim);
$phArray = array();
/* remove numeric placeholders and replace with sequential numbers */
$placeholderCounter = 0;
foreach ($placeholderArray as $value) {
if(is_numeric($value)) {
$phArray[] = $placeholderCounter;
$placeholderCounter++;
} else {
$phArray[] = $value;
}
}
if ($debug) {$output .= 'phArray: '.print_r($phArray,1).'<br />';}
/* make sure placeholder array and value array are the same size and that values are arrays */
$loopCountDiff = count($phArray) - count($loopTempArray);
if ($loopCountDiff >= 1) {
/* There are more placeholders than values. Add empty values */
for($i = $loopCountDiff; $i>=1; $i--) {
$loopTempArray[] = array(0 => '');
}
if ($debug) {$output .= 'loopTempArray (loopCountDiff >= 1): '.print_r($loopTempArray,1).'<br />';}
} elseif ($loopCountDiff <= -1) {
/* There are more values than placeholders. Add sequential number placeholders. */
for($i = $loopCountDiff; $i<=-1; $i++) {
$phArray[] = $prefix.$placeholderCounter;
$placeholderCounter++;
}
if ($debug) {$output .= 'phArray (loopCountDiff <= 1): '.print_r($phArray,1).'<br />';}
unset($placeholderCounter);
}
/* compbine placeholders and values */
$phArray;
$loopTempArray = array_combine($phArray,$loopTempArray);
if ($debug) {$output .= 'loopTempArray (with ph): '.print_r($loopTempArray,1).'<br />';}
}
/* placeholder are now definitely in array. Make sure formatting is correct (placeholder => valueArray) */
$loopArray = array();
$loopCount = 0;
if ($debug) {$output .= 'loopTempArray (two): '.print_r($loopTempArray,1).'<br />';}
foreach ($loopTempArray as $placeholder => $value) {
$loopCount++;
/* value must be an array */
if (!is_array($value)) {
$valueArray = explodeTrim(",",$value,$trim);
}
else {$valueArray = $value;}
/* placeholder must be a string */
if (is_array($placeholder)) {
$modx->log(modX::LOG_LEVEL_ERROR,'[Loop] Placeholder "'.$placeholder.'" cannot be an array.');
$loopArray[$prefix.$loopCount] = $valueArray;
} elseif (is_numeric($placeholder)) {
$loopArray[$prefix.$placeholder] = $valueArray;
} elseif (empty($placeholder)) {
$loopArray[$prefix.$loopCount] = $valueArray;
} else {
$loopArray[$placeholder] = $valueArray;
}
}
if ($debug) {$output .= 'loopArray (three): '.print_r($loopArray,1).'<br />';}
/* rearrange array to make final looping array */
$loopArrayFinal = array();
$loopValueCount = array();
foreach ($loopArray as $placeholder => $valueArray) {
$loopValueCount[$placeholder] = count($valueArray);
}
$loopMaxCount = max($loopValueCount);
if ($debug) {$output .= 'max($loopValueCount): '.$loopMaxCount.'<br />';}
$idx = 0;
for($i=$loopMaxCount;$i>0;$i--) {
$loopArrayFinal[$idx] = array();
if (!empty($useIdx)) {
$loopArrayFinal[$idx][$useIdx] = $idx + 1;
}
foreach ($loopArray as $placeholder => $valueArray) {
if (isset($valueArray[$idx])) {
$loopArrayFinal[$idx][$placeholder] = $valueArray[$idx];
} else {
$loopArrayFinal[$idx][$placeholder] = '';
}
}
$idx++;
}
if (count($loopArrayFinal) == 0) {$modx->log(modX::LOG_LEVEL_ERROR,'[Loop] returned no results.'); return $default;}
if ($debug) {$output .= 'loopArrayFinal: '.print_r($loopArrayFinal,1).'<br />';}
/* Loop through loopArrayFinal to produce templated output */
$idx = 0;
if ($phSimple) {
$staticOuter = explodeTrim('||',$static,$trim);
$staticArray = array();
foreach ($staticOuter as $value) {
$tempStaticArray = explodeTrim('==',$value,$trim);
$staticArray[$tempStaticArray[0]] = $tempStaticArray[1];
}
unset($staticOuter,$tempStaticArray);
} else {
$staticArray = $modx->fromJSON($static);
}
foreach ($loopArrayFinal as $placeholder => $valueArray) {
if ($idx > 0) {$output .= $separator;}
$idx++;
$valueArray = array_merge($staticArray,$valueArray);
if (!empty($tpl)) {
/* if template chunk is given, add chunk with placeholders to output */
$output .= $modx->getChunk($tpl,$valueArray);
} else {
/* else, just separate values with separator */
$output .= implode($valueArray,$separatorInner);
}
}
if (!empty($toPlaceholder)) {
$modx->setPlaceholder($toPlaceholder,$output);
return '';
} else return $output;