Skip to content

Commit

Permalink
Merge pull request #417 from bobthecow/fix/dotted-lambdas
Browse files Browse the repository at this point in the history
Improve support for complex lambda values
  • Loading branch information
bobthecow authored Aug 13, 2024
2 parents 1cdf391 + 8574627 commit 23f43cd
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 27 deletions.
74 changes: 53 additions & 21 deletions src/Mustache/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ private function block($nodes)
}

const SECTION_CALL = '
$value = $context->%s(%s);%s
$value = $context->%s(%s%s);%s
$buffer .= $this->section%s($context, $indent, $value);
';

Expand All @@ -333,15 +333,20 @@ private function section%s(Mustache_Context $context, $indent, $value)
if (%s) {
$source = %s;
$result = (string) call_user_func($value, $source, %s);
if (strpos($result, \'{{\') === false) {
$buffer .= $result;
} else {
$buffer .= $this->mustache
->loadLambda($result%s)
$value = call_user_func($value, $source, %s);
if (is_string($value)) {
if (strpos($value, \'{{\') === false) {
return $value;
}
return $this->mustache
->loadLambda($value%s)
->renderInternal($context);
}
} elseif (!empty($value)) {
}
if (!empty($value)) {
$values = $this->isIterable($value) ? $value : array($value);
foreach ($values as $value) {
$context->push($value);
Expand Down Expand Up @@ -390,13 +395,14 @@ private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $lev

$method = $this->getFindMethod($id);
$id = var_export($id, true);
$findArg = $this->getFindMethodArgs($method);
$filters = $this->getFilters($filters, $level);

return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key);
return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $findArg, $filters, $key);
}

const INVERTED_SECTION = '
$value = $context->%s(%s);%s
$value = $context->%s(%s%s);%s
if (empty($value)) {
%s
}
Expand All @@ -416,12 +422,13 @@ private function invertedSection($nodes, $id, $filters, $level)
{
$method = $this->getFindMethod($id);
$id = var_export($id, true);
$findArg = $this->getFindMethodArgs($method);
$filters = $this->getFilters($filters, $level);

return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level));
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $findArg, $filters, $this->walk($nodes, $level));
}

const DYNAMIC_NAME = '$this->resolveValue($context->%s(%s), $context)';
const DYNAMIC_NAME = '$this->resolveValue($context->%s(%s%s), $context)';

/**
* Generate Mustache Template dynamic name resolution PHP source.
Expand All @@ -437,12 +444,13 @@ private function resolveDynamicName($id, $dynamic)
return var_export($id, true);
}

$method = $this->getFindMethod($id);
$id = ($method !== 'last') ? var_export($id, true) : '';
$method = $this->getFindMethod($id);
$id = ($method !== 'last') ? var_export($id, true) : '';
$findArg = $this->getFindMethodArgs($method);

// TODO: filters?
return sprintf(self::DYNAMIC_NAME, $method, $id);
return sprintf(self::DYNAMIC_NAME, $method, $id, $findArg);
}

const PARTIAL_INDENT = ', $indent . %s';
Expand Down Expand Up @@ -532,7 +540,7 @@ private static function onlyBlockArgs(array $node)
}

const VARIABLE = '
$value = $this->resolveValue($context->%s(%s), $context);%s
$value = $this->resolveValue($context->%s(%s%s), $context);%s
$buffer .= %s($value === null ? \'\' : %s);
';

Expand All @@ -550,29 +558,35 @@ private function variable($id, $filters, $escape, $level)
{
$method = $this->getFindMethod($id);
$id = ($method !== 'last') ? var_export($id, true) : '';
$findArg = $this->getFindMethodArgs($method);
$filters = $this->getFilters($filters, $level);
$value = $escape ? $this->getEscape() : '$value';

return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $findArg, $filters, $this->flushIndent(), $value);
}

const FILTER = '
$filter = $context->%s(%s);
$filter = $context->%s(%s%s);
if (!(%s)) {
throw new Mustache_Exception_UnknownFilterException(%s);
}
$value = call_user_func($filter, $value);%s
$value = call_user_func($filter, %s);%s
';
const FILTER_FIRST_VALUE = '$this->resolveValue($value, $context)';
const FILTER_VALUE = '$value';

/**
* Generate Mustache Template variable filtering PHP source.
*
* If the initial $value is a lambda it will be resolved before starting the filter chain.
*
* @param string[] $filters Array of filters
* @param int $level
* @param bool $first (default: false)
*
* @return string Generated filter PHP source
*/
private function getFilters(array $filters, $level)
private function getFilters(array $filters, $level, $first = true)
{
if (empty($filters)) {
return '';
Expand All @@ -581,10 +595,12 @@ private function getFilters(array $filters, $level)
$name = array_shift($filters);
$method = $this->getFindMethod($name);
$filter = ($method !== 'last') ? var_export($name, true) : '';
$findArg = $this->getFindMethodArgs($method);
$callable = $this->getCallable('$filter');
$msg = var_export($name, true);
$value = $first ? self::FILTER_FIRST_VALUE : self::FILTER_VALUE;

return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level));
return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $findArg, $callable, $msg, $value, $this->getFilters($filters, $level, false));
}

const LINE = '$buffer .= "\n";';
Expand Down Expand Up @@ -681,6 +697,22 @@ private function getFindMethod($id)
return 'findDot';
}

/**
* Get the args needed for a given find method.
*
* In this case, it's "true" iff it's a "find dot" method and strict callables is enabled.
*
* @param string $method Find method name
*/
private function getFindMethodArgs($method)
{
if (($method === 'findDot' || $method === 'findAnchoredDot') && $this->strictCallables) {
return ', true';
}

return '';
}

const IS_CALLABLE = '!is_string(%s) && is_callable(%s)';
const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';

Expand Down
16 changes: 13 additions & 3 deletions src/Mustache/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,28 @@ public function find($id)
* ... the `name` value is only searched for within the `child` value of the global Context, not within parent
* Context frames.
*
* @param string $id Dotted variable selector
* @param string $id Dotted variable selector
* @param bool $strictCallables (default: false)
*
* @return mixed Variable value, or '' if not found
*/
public function findDot($id)
public function findDot($id, $strictCallables = false)
{
$chunks = explode('.', $id);
$first = array_shift($chunks);
$value = $this->findVariableInStack($first, $this->stack);

// This wasn't really a dotted name, so we can just return the value.
if (empty($chunks)) {
return $value;
}

foreach ($chunks as $chunk) {
if ($value === '') {
$isCallable = $strictCallables ? (is_object($value) && is_callable($value)) : (!is_string($value) && is_callable($value));

if ($isCallable) {
$value = $value();
} elseif ($value === '') {
return $value;
}

Expand Down
12 changes: 9 additions & 3 deletions src/Mustache/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,15 @@ protected function prepareContextStack($context = null)
protected function resolveValue($value, Mustache_Context $context)
{
if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
return $this->mustache
->loadLambda((string) call_user_func($value))
->renderInternal($context);
$result = call_user_func($value);

if (is_string($result)) {
return $this->mustache
->loadLambda($result)
->renderInternal($context);
}

return $result;
}

return $value;
Expand Down
51 changes: 51 additions & 0 deletions test/Mustache/Test/FiveThree/Functional/FiltersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,55 @@ public function brokenPipeData()
})),
);
}

/**
* @group lambdas
* @dataProvider lambdaFiltersData
*/
public function testLambdaFilters($tpl, $data, $expect)
{
$this->assertEquals($expect, $this->mustache->render($tpl, $data));
}

public function lambdaFiltersData()
{
$people = array(
(object) array('name' => 'Albert'),
(object) array('name' => 'Betty'),
(object) array('name' => 'Charles'),
);

$data = array(
'noop' => function ($value) {
return $value;
},
'people' => $people,
'people_lambda' => function () use ($people) {
return $people;
},
'first_name' => function ($arr) {
return $arr[0]->name;
},
'last_name' => function ($arr) {
$last = end($arr);

return $last->name;
},
'all_names' => function ($arr) {
return implode(', ', array_map(function ($person) { return $person->name; }, $arr));
},
'first_person' => function ($arr) {
return $arr[0];
},
);

return array(
array('{{% FILTERS }}{{ people | first_name }}', $data, 'Albert'),
array('{{% FILTERS }}{{ people | last_name }}', $data, 'Charles'),
array('{{% FILTERS }}{{ people | all_names }}', $data, 'Albert, Betty, Charles'),
array('{{% FILTERS }}{{# people | first_person }}{{ name }}{{/ people }}', $data, 'Albert'),
array('{{% FILTERS }}{{# people_lambda | first_person }}{{ name }}{{/ people_lambda }}', $data, 'Albert'),
array('{{% FILTERS }}{{# people_lambda | noop | first_person }}{{ name }}{{/ people_lambda }}', $data, 'Albert'),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,33 @@ public function testViewArrayAnonymousSectionCallback()

$this->assertEquals(sprintf('[[%s]]', $data['name']), $tpl->render($data));
}

/**
* @dataProvider nonTemplateLambdasData
*/
public function testNonTemplateLambdas($tpl, $data, $expect)
{
$this->assertEquals($expect, $this->mustache->render($tpl, $data));
}

public function nonTemplateLambdasData()
{
$data = array(
'lang' => 'en-US',
'people' => function () {
return array(
(object) array('name' => 'Albert', 'lang' => 'en-GB'),
(object) array('name' => 'Betty'),
(object) array('name' => 'Charles'),
);
},
);

return array(
array("{{# people }} - {{ name }}\n{{/people}}", $data, " - Albert\n - Betty\n - Charles\n"),
array("{{# people }} - {{ name }}: {{ lang }}\n{{/people}}", $data, " - Albert: en-GB\n - Betty: en-US\n - Charles: en-US\n"),
);
}
}

class Mustache_Test_FiveThree_Functional_Foo
Expand Down

0 comments on commit 23f43cd

Please sign in to comment.