diff --git a/src/Pluf/Template/Compiler.php b/src/Pluf/Template/Compiler.php index 9ad2d81..e790e1a 100644 --- a/src/Pluf/Template/Compiler.php +++ b/src/Pluf/Template/Compiler.php @@ -48,7 +48,7 @@ class Pluf_Template_Compiler protected $_vartype = array(T_CHARACTER, T_CONSTANT_ENCAPSED_STRING, T_DNUMBER, T_ENCAPSED_AND_WHITESPACE, T_LNUMBER, T_OBJECT_OPERATOR, T_STRING, - T_WHITESPACE, T_ARRAY, T_CLASS, T_PRIVATE); + T_WHITESPACE, T_ARRAY, T_CLASS, T_PRIVATE, T_LIST); /** * Assignation operators. @@ -435,7 +435,7 @@ class Pluf_Template_Compiler $res = 'elseif('.$this->_parseFinal($args, $this->_allowedInExpr).'):'; break; case 'foreach': - $res = 'foreach ('.$this->_parseFinal($args, array_merge(array(T_AS, T_DOUBLE_ARROW, T_STRING, T_OBJECT_OPERATOR, $this->_allowedAssign, '[', ']')), array(';','!')).'): '; + $res = 'foreach ('.$this->_parseFinal($args, array_merge(array(T_AS, T_DOUBLE_ARROW, T_STRING, T_OBJECT_OPERATOR, T_LIST, $this->_allowedAssign, '[', ']')), array(';','!')).'): '; array_push($this->_blockStack, 'foreach'); break; case 'while': diff --git a/src/Pluf/Template/Tag/Cycle.php b/src/Pluf/Template/Tag/Cycle.php new file mode 100644 index 0000000..26c63ba --- /dev/null +++ b/src/Pluf/Template/Tag/Cycle.php @@ -0,0 +1,153 @@ +cycle. + * + * Cycle among the given strings or variables each time this tag is + * encountered. + * + * Within a loop, cycles among the given strings each time through the loop: + * + * + * {foreach $some_list as $obj} + * + * ... + * + * {/foreach} + * + * + * You can use variables, too. For example, if you have two + * template variables, $rowvalue1 and $rowvalue2, you can + * cycle between their values like this: + * + * + * {foreach $some_list as $obj} + * + * ... + * + * {/foreach} + * + * + * You can mix variables and strings: + * + * + * {foreach $some_list as $obj} + * + * ... + * + * {/foreach} + * + * + * In some cases you might want to refer to the next value of a cycle + * from outside of a loop. To do this, just group the arguments into + * an array and give the {cycle} tag name last, like this: + * + * + * {cycle array('row1', 'row2'), 'rowcolors'} + * + * + * From then on, you can insert the current value of the cycle + * wherever you'd like in your template: + * + * + * ... + * ... + * + * Based on concepts from the Django cycle template tag. + */ +class Pluf_Template_Tag_Cycle extends Pluf_Template_Tag +{ + /** + * @see Pluf_Template_Tag::start() + * @throws InvalidArgumentException If no argument is provided. + */ + public function start() + { + $nargs = func_num_args(); + if (1 > $nargs) { + throw new InvalidArgumentException( + '`cycle` tag requires at least one argument' + ); + } + + $result = ''; + list($key, $index) = $this->_computeIndex(func_get_args()); + + switch ($nargs) { + # (array or mixed) argument + case 1: + $arg = func_get_arg(0); + if (is_array($arg)) { + $result = $arg[$index % count($arg)]; + } else { + $result = $arg; + } + break; + + # (array) arguments, (string) assign + case 2: + $args = func_get_args(); + if (is_array($args[0])) { + $last = array_pop($args); + if (is_string($last) && '' === $this->context->get($last)) { + $value = Pluf_Utils::flattenArray($args[0]); + $this->context->set($last, $value); + + list($assign_key, $assign_index) = $this->_computeIndex(array($value)); + $result = $value[0]; + } + break; + } + + # considers all the arguments as a value to use in the cycle + default: + $args = Pluf_Utils::flattenArray(func_get_args()); + $result = $args[$index % count($args)]; + break; + } + + echo Pluf_Template::markSafe((string) $result); + } + + /** + * Compute an index for the given array. + * + * @param array + * @return array A array of two elements: key and index. + */ + protected function _computeIndex($args) + { + if (!isset($this->context->__cycle_stack)) { + $this->context->__cycle_stack = array(); + } + + $key = serialize($args); + $this->context->__cycle_stack[$key] = (array_key_exists($key, $this->context->__cycle_stack)) ? + 1 + $this->context->__cycle_stack[$key] : + 0; + $index = $this->context->__cycle_stack[$key]; + + return array($key, $index); + } +} diff --git a/src/Pluf/Template/Tag/Firstof.php b/src/Pluf/Template/Tag/Firstof.php new file mode 100644 index 0000000..40af976 --- /dev/null +++ b/src/Pluf/Template/Tag/Firstof.php @@ -0,0 +1,79 @@ +firstof. + * + * Outputs the first variable passed that is not false, without escaping. + * Outputs nothing if all the passed variables are false. + * + * Sample usage: + * + * {firstof array($var1, $var2, $var3)} + * + * This is equivalent to: + * + * + * {if $var1} + * {$var1|safe} + * {elseif $var2} + * {$var2|safe} + * {elseif $var3} + * {$var3|safe} + * {/if} + * + * + * You can also use a literal string as a fallback value in case all + * passed variables are false: + * + * {firstof array($var1, $var2, $var3), "fallback value"} + * + * Based on concepts from the Django firstof template tag. + */ +class Pluf_Template_Tag_Firstof extends Pluf_Template_Tag +{ + /** + * @see Pluf_Template_Tag::start() + * @param string $token Variables to test. + * @param string $fallback Literal string to used when all passed variables are false. + * @throws InvalidArgumentException If no argument is provided. + */ + public function start($tokens = array(), $fallback = null) + { + if (!is_array($tokens) || 0 === count($tokens)) { + throw new InvalidArgumentException( + '`firstof` tag requires at least one array as argument' + ); + } + $result = (string) $fallback; + + foreach ($tokens as $var) { + if ($var) { + $result = Pluf_Template::markSafe((string) $var); + break; + } + } + + echo $result; + } +} diff --git a/src/Pluf/Template/Tag/Now.php b/src/Pluf/Template/Tag/Now.php new file mode 100644 index 0000000..5126b32 --- /dev/null +++ b/src/Pluf/Template/Tag/Now.php @@ -0,0 +1,46 @@ +now. + * + * Displays the date, formatted according to the given string. + * + * Sample usage: + * It is {now "jS F Y H:i"} + * + * Based on concepts from the Django now template tag. + * + * @link http://php.net/date for all the possible values. + */ +class Pluf_Template_Tag_Now extends Pluf_Template_Tag +{ + /** + * @see Pluf_Template_Tag::start() + * @param string $token Format to be applied. + */ + public function start($token) + { + echo date($token); + } +} diff --git a/src/Pluf/Template/Tag/Regroup.php b/src/Pluf/Template/Tag/Regroup.php new file mode 100644 index 0000000..32cf857 --- /dev/null +++ b/src/Pluf/Template/Tag/Regroup.php @@ -0,0 +1,165 @@ +regroup. + * + * Regroup a list of alike objects by a common attribute. + * + * This complex tag is best illustrated by use of an example: + * say that people is a list of people represented by arrays with + * first_name, last_name, and gender keys: + * + * + * $people = array( + * array('first_name' => 'George', + * 'last_name' => 'Bush', + * 'gender' => 'Male'), + * array('first_name' => 'Bill', + * 'last_name' => 'Clinton', + * 'gender' => 'Male'), + * array('first_name' => 'Margaret', + * 'last_name' => 'Thatcher', + * 'gender' => 'Female'), + * array('first_name' => 'Condoleezza', + * 'last_name' => 'Rice', + * 'gender' => 'Female'), + * array('first_name' => 'Pat', + * 'last_name' => 'Smith', + * 'gender' => 'Unknow'), + * ); + * + * + * ...and you'd like to display a hierarchical list that is ordered by + * gender, like this: + * + * + * + * You can use the {regroup} tag to group the list of people by + * gender. The following snippet of template code would accomplish this: + * + * + * {regroup $people, 'gender', 'gender_list'} + * + * + * + * Let's walk through this example. {regroup} takes three arguments: + * the object (array or instance of Pluf_Model or any object) + * you want to regroup, the attribute to group by,and the name of the + * resulting object. Here, we're regrouping the people list by the + * gender attribute and calling the result gender_list. The result is + * assigned in a context varible of the same name $gender_list. + * + * {regroup} produces a instance of ArrayObject (in this case, $gender_list) + * of group objects. Each group object has two attributes: + * + * + * + * Note that {regroup} does not order its input! + * + * Based on concepts from the Django regroup template tag. + */ +class Pluf_Template_Tag_Regroup extends Pluf_Template_Tag +{ + /** + * @see Pluf_Template_Tag::start() + * @param mixed $data The object to group. + * @param string $by The attribute ti group by. + * @param string $assign The name of the resulting object. + * @throws InvalidArgumentException If no argument is provided. + */ + public function start($data, $by, $assign) + { + $grouped = array(); + $tmp = array(); + + foreach ($data as $group) { + if (is_object($group)) { + if (is_subclass_of($group, 'Pluf_Model')) { + $raw = $group->getData(); + if (!array_key_exists($by, $raw)) { + continue; + } + } else { + $ref = new ReflectionObject($group); + if (!$ref->hasProperty($by)) { + continue; + } + } + $key = $group->$by; + $list = $group; + } else { + if (!array_key_exists($by, $group)) { + continue; + } + $key = $group[$by]; + $list = new ArrayObject($group, ArrayObject::ARRAY_AS_PROPS); + } + + if (!array_key_exists($key, $tmp)) { + $tmp[$key] = array(); + } + $tmp[$key][] = $list; + } + + foreach ($tmp as $key => $list) { + $grouped[] = new ArrayObject(array('grouper' => $key, + 'list' => $list), + ArrayObject::ARRAY_AS_PROPS); + } + $this->context->set(trim($assign), $grouped); + } +} diff --git a/src/Pluf/Test/TemplatetagsUnitTestCase.php b/src/Pluf/Test/TemplatetagsUnitTestCase.php new file mode 100644 index 0000000..9030e15 --- /dev/null +++ b/src/Pluf/Test/TemplatetagsUnitTestCase.php @@ -0,0 +1,91 @@ +tag_name.'` template tag.'; + parent::__construct($label); + + if (null === $this->tag_name) { + throw new LogicException('You must initialize the `$tag_name` property.'); + } + if (null === $this->tag_class) { + throw new LogicException('You must initialize the `$tag_class` property.'); + } + + $folder = Pluf::f('tmp_folder').'/templatetags'; + if (!file_exists($folder)) { + mkdir($folder, 0777, true); + } + $this->tpl_folders = array($folder); + + Pluf_Signal::connect('Pluf_Template_Compiler::construct_template_tags_modifiers', + array($this, 'addTemplatetag')); + } + + public function addTemplatetag($signal, &$params) + { + $params['tags'] = array_merge($params['tags'], + array($this->tag_name => $this->tag_class)); + } + + protected function writeTemplateFile($tpl_name, $content) + { + $file = $this->tpl_folders[0].'/'.$tpl_name; + if (file_exists($file)) { + unlink($file); + } + file_put_contents($file, $content); + } + + protected function getNewTemplate($content = '') + { + $tpl_name = sprintf('%s-%s.html', + get_class($this), + md5($content.microtime(true))); + $this->writeTemplateFile($tpl_name, $content); + + return new Pluf_Template($tpl_name, $this->tpl_folders); + } +} diff --git a/src/Pluf/Tests/TemplateTags/Cycle.php b/src/Pluf/Tests/TemplateTags/Cycle.php new file mode 100644 index 0000000..f2540df --- /dev/null +++ b/src/Pluf/Tests/TemplateTags/Cycle.php @@ -0,0 +1,166 @@ +skipIf(1, "%s\n " . $message); + } + } + + public function testNoArguments() + { + $tpl = $this->getNewTemplate('{cycle}'); + try { + $tpl->render(); + $this->fail(); + } catch (InvalidArgumentException $e) { + $this->pass(); + } + } + + public function testSimpleCaseInLoop() + { + $context = new Pluf_Template_Context(array('test' => range(0, 4))); + $to_parse = '{foreach $test as $i}'. + '{cycle "a", "b"}{$i},'. + '{/foreach}'; + $expected = 'a0,b1,a2,b3,a4,'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testSingleStringArgument() + { + $context = new Pluf_Template_Context(array('test' => range(0, 4))); + $to_parse = '{foreach $test as $i}'. + '{cycle "a"}{$i},'. + '{/foreach}'; + $expected = 'a0,a1,a2,a3,a4,'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testSingleArrayArgument() + { + $context = new Pluf_Template_Context(array('test' => range(0, 4))); + $to_parse = '{foreach $test as $i}'. + '{cycle array("a", "b", "c")}{$i},'. + '{/foreach}'; + $expected = 'a0,b1,c2,a3,b4,'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testSingleContextVariableArgument() + { + $context = new Pluf_Template_Context(array('one' => 1)); + $to_parse = '{cycle $one}{cycle $one}'; + $expected = '11'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testMultipleCalls() + { + $to_parse = '{cycle "a", "b"}{cycle "a", "b"}'; + $expected = 'ab'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render()); + } + + public function testAssignContextVariable() + { + $to_parse = '{cycle array("a", "b", "c"), "abc"}'. + '{cycle $abc}'; + $expected = 'ab'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render()); + + $to_parse = '{cycle array("a", "b", "c"), "abc"}'. + '{cycle $abc}'. + '{cycle $abc}'; + $expected = 'abc'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render()); + + $to_parse = '{cycle array("a", "b", "c"), "abc"}'. + '{cycle $abc}'. + '{cycle $abc}'. + '{cycle $abc}'; + $expected = 'abca'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render()); + } + + public function testContextVariablesInArrayAsArgument() + { + $context = new Pluf_Template_Context(array('test' => range(0, 4), + 'one' => 1, + 'two' => 2)); + $to_parse = '{foreach $test as $i}'. + '{cycle array($one, $two)}'. + '{/foreach}'; + $expected = '12121'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + + $context = new Pluf_Template_Context(array('one' => 1, + 'two' => 2)); + $to_parse = '{cycle array($one, $two), "counter"}{cycle $counter}'; + $expected = '12'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testContextVariablesArgument() + { + $context = new Pluf_Template_Context(array('test' => range(0, 4), + 'first' => 'a', + 'second' => 'b')); + $to_parse = '{foreach $test as $i}'. + '{cycle $first, $second}{$i},'. + '{/foreach}'; + $expected = 'a0,b1,a2,b3,a4,'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testFilterInCycle() + { + $this->skip('Pluf has no support for applying filters to a variable of array'); + return; + + $context = new Pluf_Template_Context(array('one' => 'A', + 'two' => '2')); + $to_parse = '{cycle array($one|lower, $two), "counter"}{cycle $counter}'; + $expected = 'a2'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } +} diff --git a/src/Pluf/Tests/TemplateTags/Firstof.php b/src/Pluf/Tests/TemplateTags/Firstof.php new file mode 100644 index 0000000..6201e17 --- /dev/null +++ b/src/Pluf/Tests/TemplateTags/Firstof.php @@ -0,0 +1,98 @@ +getNewTemplate('{firstof}'); + try { + $tpl->render(); + $this->fail(); + } catch (InvalidArgumentException $e) { + $this->pass(); + } + } + + public function testOutputsNothing() + { + $context = new Pluf_Template_Context(array('a' => 0, + 'b' => 0, + 'c' => 0)); + $to_parse = '{firstof array($a, $b, $c)}'; + $expected = ''; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testOutputsMatched() + { + $to_parse = '{firstof array($a, $b, $c)}'; + + $context = new Pluf_Template_Context(array('a' => 1, + 'b' => 0, + 'c' => 0)); + $expected = '1'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + + $context = new Pluf_Template_Context(array('a' => 0, + 'b' => 2, + 'c' => 0)); + $expected = '2'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + + $context = new Pluf_Template_Context(array('a' => 0, + 'b' => 0, + 'c' => 3)); + $expected = '3'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testOutputsFirstMatch() + { + $context = new Pluf_Template_Context(array('a' => 1, + 'b' => 2, + 'c' => 3)); + $to_parse = '{firstof array($a, $b, $c)}'; + $expected = '1'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testOutputsFallback() + { + $context = new Pluf_Template_Context(array('a' => 0, + 'b' => 0, + 'c' => 0)); + $to_parse = '{firstof array($a, $b, $c), "my fallback"}'; + $expected = 'my fallback'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } +} diff --git a/src/Pluf/Tests/TemplateTags/Now.php b/src/Pluf/Tests/TemplateTags/Now.php new file mode 100644 index 0000000..d990152 --- /dev/null +++ b/src/Pluf/Tests/TemplateTags/Now.php @@ -0,0 +1,49 @@ +getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render()); + } + + public function testParsingEscapedCharaters() + { + $to_parse = '{now "j \"n\" Y"}'; + $expected = date("j \"n\" Y"); + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render()); + + $to_parse = '{now "j \nn\n Y"}'; + $tpl = $this->getNewTemplate($to_parse); + $expected = date("j \nn\n Y"); + $this->assertEqual($expected, $tpl->render()); + } +} diff --git a/src/Pluf/Tests/TemplateTags/Regroup.php b/src/Pluf/Tests/TemplateTags/Regroup.php new file mode 100644 index 0000000..78e9614 --- /dev/null +++ b/src/Pluf/Tests/TemplateTags/Regroup.php @@ -0,0 +1,208 @@ +_a['verbose'] = 'people'; + $this->_a['table'] = 'people'; + $this->_a['model'] = __CLASS__; + $this->_a['cols'] = array( + 'id' => array( + 'type' => 'Pluf_DB_Field_Sequence', + 'blank' => true, + ), + 'first_name' => array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => true, + 'size' => 50, + ), + 'last_name' => array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => true, + 'size' => 50, + ), + 'gender' => array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => true, + 'size' => 50, + 'default' => 'Unknown', + ), + ); + } +} + +class Pluf_Tests_Templatetags_Regroup extends Pluf_Test_TemplatetagsUnitTestCase +{ + protected $tag_class = 'Pluf_Template_Tag_Regroup'; + protected $tag_name = 'regroup'; + + public function testRegroupAnArray() + { + $context = new Pluf_Template_Context(array( + 'data' => array(array('foo' => 'c', 'bar' => 1), + array('foo' => 'd', 'bar' => 1), + array('foo' => 'a', 'bar' => 2), + array('foo' => 'b', 'bar' => 2), + array('foo' => 'x', 'bar' => 3)))); + $to_parse = '{regroup $data, "bar", "grouped"}'. + '{foreach $grouped as $group}'. + '{$group.grouper}:'. + '{foreach $group.list as $item}'. + '{$item.foo}'. + '{/foreach},'. + '{/foreach}'; + $expected = '1:cd,2:ab,3:x,'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testRegroupAnObject() + { + $obj1 = new stdClass(); + $obj1->foo = 'c'; + $obj1->bar = 1; + $obj2 = new stdClass(); + $obj2->foo = 'd'; + $obj2->bar = 1; + + $obj3 = new stdClass(); + $obj3->foo = 'a'; + $obj3->bar = 2; + $obj4 = new stdClass(); + $obj4->foo = 'b'; + $obj4->bar = 2; + + $obj5 = new stdClass(); + $obj5->foo = 'x'; + $obj5->bar = 3; + + $context = new Pluf_Template_Context(array( + 'data' => array($obj1, $obj2, $obj3, $obj4, $obj5))); + $to_parse = '{regroup $data, "bar", "grouped"}'. + '{foreach $grouped as $group}'. + '{$group.grouper}:'. + '{foreach $group.list as $item}'. + '{$item.foo}'. + '{/foreach},'. + '{/foreach}'; + $expected = '1:cd,2:ab,3:x,'; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + } + + public function testRegroupPlufModelInstance() + { + $db = Pluf::db(); + $schema = new Pluf_DB_Schema($db); + $m = new Pluf_Tests_Model_People_Model(); + $schema->model = $m; + $schema->createTables(); + + $people = array( + array('first_name' => 'George', + 'last_name' => 'Bush', + 'gender' => 'Male'), + array('first_name' => 'Bill', + 'last_name' => 'Clinton', + 'gender' => 'Male'), + array('first_name' => 'Margaret', + 'last_name' => 'Thatcher', + 'gender' => 'Female'), + array('first_name' => 'Condoleezza', + 'last_name' => 'Rice', + 'gender' => 'Female'), + array('first_name' => 'Pat', + 'last_name' => 'Smith', + 'gender' => 'Unknow'), + ); + + foreach ($people as $person) { + $p = new Pluf_Tests_Model_People_Model(); + foreach ($person as $key => $value) { + $p->$key = $value; + } + $p->create(); + } + unset($p); + + $people_list = Pluf::factory('Pluf_Tests_Model_People_Model')->getList(); + $context = new Pluf_Template_Context(array( + 'people' => $people_list)); + $to_parse = << +{foreach \$gender_list as \$gender} +
  • {\$gender.grouper}: + +
  • +{/foreach} + +TPL; + $expected = << + +
  • Male: + +
  • + +
  • Female: + +
  • + +
  • Unknow: + +
  • + + +HTML; + $tpl = $this->getNewTemplate($to_parse); + $this->assertEqual($expected, $tpl->render($context)); + $schema->dropTables(); + } + +} diff --git a/src/Pluf/Utils.php b/src/Pluf/Utils.php index bdc4e9c..95d4765 100644 --- a/src/Pluf/Utils.php +++ b/src/Pluf/Utils.php @@ -289,4 +289,20 @@ class Pluf_Utils return base64_decode($data); } + /** + * Flatten an array. + * + * @param array $array The array to flatten. + * @return array + */ + public static function flattenArray($array) + { + $result = array(); + foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($array)) as $value) { + $result[] = $value; + } + + return $result; + } + }