Skip to content

Commit 06dbdd6

Browse files
authored
Merge pull request #59 from sanmai/fix-include-obj-rebase
Context: defer object-to-string conversion
2 parents 67acc44 + 3d96a0f commit 06dbdd6

File tree

9 files changed

+139
-33
lines changed

9 files changed

+139
-33
lines changed

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
* master
22

3+
* 1.4.0 (2017-09-25)
4+
35
* Dropped support for EOL'ed versions of PHP (< 5.6)
6+
* Arrays won't be silently cast to string as 'Array' anymore
7+
* Complex objects could now be passed between templates and to filters
8+
* Additional test coverage
49

510
* 1.3.1 (2017-09-23)
611

src/Liquid/AbstractBlock.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,17 @@ protected function renderAll(array $list, Context $context) {
114114
$result = '';
115115

116116
foreach ($list as $token) {
117-
$result .= (is_object($token) && method_exists($token, 'render')) ? $token->render($context) : $token;
117+
if (method_exists($token, 'render')) {
118+
$value = $token->render($context);
119+
} else {
120+
$value = $token;
121+
}
122+
123+
if (is_array($value)) {
124+
throw new LiquidException("Implicit rendering of arrays not supported. Use index operator.");
125+
}
126+
127+
$result .= $value;
118128

119129
if (isset($context->registers['break'])) {
120130
break;

src/Liquid/AbstractTag.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ public function parse(array &$tokens) {
6666
*
6767
* @return string
6868
*/
69-
public function render(Context $context) {
70-
return '';
71-
}
69+
abstract public function render(Context $context);
7270

7371
/**
7472
* Extracts tag attributes from a markup string.

src/Liquid/Context.php

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ private function fetch($key) {
234234
*
235235
* @param string $key
236236
*
237+
* @see Decision::stringValue
238+
* @see AbstractBlock::renderAll
239+
*
237240
* @throws LiquidException
238241
* @return mixed
239242
*/
@@ -333,23 +336,22 @@ private function variable($key) {
333336
// we'll try casting this object in the next iteration
334337
}
335338

336-
// Traversable objects are taken care of inside filters
337-
if ($object instanceof \Traversable) {
338-
return $object;
339-
}
340-
341-
// finally, resolve an object to a string or a plain value
342-
if (method_exists($object, '__toString')) {
343-
$object = (string) $object;
344-
} elseif (method_exists($object, 'toLiquid')) {
339+
// lastly, try to get an embedded value of an object
340+
// value could be of any type, not just string, so we have to do this
341+
// conversion here, not laster in AbstractBlock::renderAll
342+
if (method_exists($object, 'toLiquid')) {
345343
$object = $object->toLiquid();
346344
}
347345

348-
// if everything else fails, throw up
349-
if (is_object($object)) {
350-
$class = get_class($object);
351-
throw new LiquidException("Value of type $class has no `toLiquid` nor `__toString` methods");
352-
}
346+
/*
347+
* Before here were checks for object types and object to string conversion.
348+
*
349+
* Now we just return what we have:
350+
* - Traversable objects are taken care of inside filters
351+
* - Object-to-string conversion is handled at the last moment in Decision::stringValue, and in AbstractBlock::renderAll
352+
*
353+
* This way complex objects could be passed between templates and to filters
354+
*/
353355

354356
return $object;
355357
}

src/Liquid/Decision.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ private function stringValue($value) {
4444
if (method_exists($value, '__toString')) {
4545
$value = (string) $value;
4646
} else {
47-
throw new LiquidException("Cannot convert $value to string"); // harry
47+
// toLiquid is handled in Context::variable
48+
$class = get_class($value);
49+
throw new LiquidException("Value of type $class has no `toLiquid` nor `__toString` methods");
4850
}
4951
}
5052

tests/Liquid/ContextTest.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,9 @@ public function testGetSetObject() {
166166
$this->assertNull($this->context->get('object.invalid'));
167167
}
168168

169-
/**
170-
* @expectedException \Liquid\LiquidException
171-
*/
172-
public function testFinalVariableIsObject() {
169+
public function testFinalVariableCanBeObject() {
173170
$this->context->set('test', (object) array('value' => (object) array()));
174-
$this->context->get('test.value');
171+
$this->assertInstanceOf(\stdClass::class, $this->context->get('test.value'));
175172
}
176173

177174
public function testVariables() {

tests/Liquid/Tag/TagCaseTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ public function __toString() {
2020
}
2121
}
2222

23+
class HasToLiquid
24+
{
25+
public function toLiquid() {
26+
return "100";
27+
}
28+
}
29+
2330
class TagCaseTest extends TestCase
2431
{
2532
public function testCase() {
@@ -78,4 +85,8 @@ public function testObject() {
7885
public function testStringable() {
7986
$this->assertTemplateResult('hit', '{% case variable %}{% when 100 %}hit{% endcase %}', array('variable' => new Stringable()));
8087
}
88+
89+
public function testToLiquid() {
90+
$this->assertTemplateResult('hit', '{% case variable %}{% when 100 %}hit{% endcase %}', array('variable' => new HasToLiquid()));
91+
}
8192
}

tests/Liquid/Tag/TagIncludeTest.php

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,17 @@
1616
use Liquid\Liquid;
1717
use Liquid\Cache\Local;
1818
use Liquid\FileSystem\Virtual;
19+
use Liquid\TestFileSystem;
1920

2021
class TagIncludeTest extends TestCase
2122
{
2223
private $fs;
2324

2425
protected function setUp() {
25-
$this->fs = new Virtual(function ($templatePath) {
26-
if ($templatePath == 'inner') {
27-
return "Inner: {{ inner }}{{ other }}";
28-
}
29-
30-
if ($templatePath == 'example') {
31-
return "Example: {% include 'inner' %}";
32-
}
33-
});
26+
$this->fs = TestFileSystem::fromArray(array(
27+
'inner' => "Inner: {{ inner }}{{ other }}",
28+
'example' => "Example: {% include 'inner' %}",
29+
));
3430
}
3531

3632
protected function tearDown() {
@@ -115,4 +111,60 @@ public function testIncludeTemplateFile() {
115111
// template include inserts a new line
116112
$this->assertEquals("test content\n", $template->render());
117113
}
114+
115+
public function testIncludePassPlainValue() {
116+
$template = new Template();
117+
$template->setFileSystem(TestFileSystem::fromArray(array(
118+
'inner' => "[{{ other }}]",
119+
'example' => "({% include 'inner' other:var %})",
120+
)));
121+
122+
$template->parse("{% include 'example' %}");
123+
124+
$output = $template->render(array("var" => "test"));
125+
$this->assertEquals("([test])", $output);
126+
}
127+
128+
/**
129+
* @expectedException \Liquid\LiquidException
130+
* @expectedExceptionMessage Use index operator
131+
*/
132+
public function testIncludePassArrayWithoutIndex() {
133+
134+
$template = new Template();
135+
$template->setFileSystem(TestFileSystem::fromArray(array(
136+
'inner' => "[{{ other }}]",
137+
'example' => "({% include 'inner' other:var %})",
138+
)));
139+
140+
$template->parse("{% include 'example' %}");
141+
$template->render(array("var" => array("a", "b", "c")));
142+
}
143+
144+
public function testIncludePassArrayWithIndex() {
145+
146+
$template = new Template();
147+
$template->setFileSystem(TestFileSystem::fromArray(array(
148+
'inner' => "[{{ other[0] }}]",
149+
'example' => "({% include 'inner' other:var %})",
150+
)));
151+
152+
$template->parse("{% include 'example' %}");
153+
154+
$output = $template->render(array("var" => array("a", "b", "c")));
155+
$this->assertEquals("([a])", $output);
156+
}
157+
158+
public function testIncludePassObjectValue() {
159+
$template = new Template();
160+
$template->setFileSystem(TestFileSystem::fromArray(array(
161+
'inner' => "[{{ other.a }}]",
162+
'example' => "({% include 'inner' other:var %})",
163+
)));
164+
165+
$template->parse("{% include 'example' %}");
166+
167+
$output = $template->render(array("var" => (object) array('a' => 'b')));
168+
$this->assertEquals("([b])", $output);
169+
}
118170
}

tests/Liquid/TestFileSystem.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Liquid package.
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*
9+
* @package Liquid
10+
*/
11+
12+
namespace Liquid;
13+
14+
use Liquid\FileSystem\Virtual;
15+
16+
class TestFileSystem extends Virtual
17+
{
18+
/** @return TestFileSystem */
19+
public static function fromArray($array)
20+
{
21+
return new static(function ($path) use ($array) {
22+
if (isset($array[$path])) {
23+
return $array[$path];
24+
}
25+
26+
return '';
27+
});
28+
}
29+
}

0 commit comments

Comments
 (0)