Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for complex lambda values #417

Merged
merged 4 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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