Skip to content

Commit 139797a

Browse files
authored
Support for Laravel Collections style object chaining (#1168)
* Support for Laravel Collections style object chaining for objects return from function calls implemented as modifiers Fixes #1151 * explain publishing docs
1 parent 6709d00 commit 139797a

6 files changed

Lines changed: 971 additions & 735 deletions

File tree

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,7 @@ If you are a maintainer, you can publish the document using [mike](https://githu
115115
mike deploy 5.x
116116
```
117117

118+
Then, push the `gh-pages` branch.
119+
118120
## Attribution
119121
This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)!

changelog/1151.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Support for Laravel Collections style object chaining for objects return from function calls implemented as modifiers [#1151](https://github.com/smarty-php/smarty/issues/1151)

src/Parser/TemplateParser.php

Lines changed: 740 additions & 729 deletions
Large diffs are not rendered by default.

src/Parser/TemplateParser.y

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,12 +1062,22 @@ object(res) ::= varindexed(vi) objectchain(oc). {
10621062
}
10631063
}
10641064

1065+
// optional objectchain - empty
1066+
optobjectchain(res) ::= . {
1067+
res = '';
1068+
}
1069+
1070+
// optional objectchain - present
1071+
optobjectchain(res) ::= objectchain(oc). {
1072+
res = oc;
1073+
}
1074+
10651075
// single element
10661076
objectchain(res) ::= objectelement(oe). {
10671077
res = oe;
10681078
}
10691079

1070-
// chain of elements
1080+
// chain of elements
10711081
objectchain(res) ::= objectchain(oc) objectelement(oe). {
10721082
res = oc.oe;
10731083
}
@@ -1111,7 +1121,7 @@ objectelement(res)::= PTR method(f). {
11111121
//
11121122
// function
11131123
//
1114-
function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP. {
1124+
function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP optobjectchain(oc). {
11151125

11161126
if (f == 'isset') {
11171127
res = '(true';
@@ -1125,15 +1135,15 @@ function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP. {
11251135
res .= ' && (' . $value . ' !== null)';
11261136
}
11271137
}
1128-
res .= ')';
1138+
res .= ')' . oc;
11291139
} elseif (f == 'empty') {
11301140
if (count(v) != 1) {
11311141
throw new CompilerException("Invalid number of arguments for empty. empty expects at exactly one parameter.");
11321142
}
11331143
if (is_array(v[0])) {
1134-
res .= '( !' . v[0][0] . ' || empty(' . v[0][1] . '))';
1144+
res = '( !' . v[0][0] . ' || empty(' . v[0][1] . '))' . oc;
11351145
} else {
1136-
res = 'false == ' . v[0];
1146+
res = 'false == ' . v[0] . oc;
11371147
}
11381148
} else {
11391149
$p = array();
@@ -1144,7 +1154,7 @@ function(res) ::= ns1(f) OPENP variablelist(v) CLOSEP. {
11441154
$p[] = $value;
11451155
}
11461156
}
1147-
res = $this->compiler->compileModifierInExpression(f, $p);
1157+
res = $this->compiler->compileModifierInExpression(f, $p) . oc;
11481158
}
11491159
}
11501160

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<?php
2+
/**
3+
* Smarty PHPunit tests for object chain functionality after function calls
4+
* Tests the new feature: {$x = collect($data)->filter()->values()->toJson()}
5+
*
6+
* @author Smarty Vibe
7+
*/
8+
9+
/**
10+
* Helper class to simulate a chainable collection object
11+
*/
12+
class ChainableCollection
13+
{
14+
private $data;
15+
16+
public function __construct($data)
17+
{
18+
$this->data = $data;
19+
}
20+
21+
public function filter()
22+
{
23+
$this->data = array_filter($this->data);
24+
return $this;
25+
}
26+
27+
public function values()
28+
{
29+
$this->data = array_values($this->data);
30+
return $this;
31+
}
32+
33+
public function toJson()
34+
{
35+
return json_encode($this->data);
36+
}
37+
38+
public function toArray()
39+
{
40+
return $this->data;
41+
}
42+
43+
public function count()
44+
{
45+
return count($this->data);
46+
}
47+
}
48+
49+
/**
50+
* Modifier plugin that returns a chainable object (simulates collect())
51+
*/
52+
function smarty_modifier_collect($data)
53+
{
54+
return new ChainableCollection($data);
55+
}
56+
57+
/**
58+
* Modifier plugin that returns an object with methods
59+
*/
60+
function smarty_modifier_create_object($value = null)
61+
{
62+
return new class {
63+
public function getName() {
64+
return 'TestObject';
65+
}
66+
67+
public function getNext() {
68+
return new class {
69+
public function getValue() {
70+
return 'ChainedValue';
71+
}
72+
};
73+
}
74+
};
75+
}
76+
77+
/**
78+
* class for function object chain tests
79+
*
80+
* @preserveGlobalState disabled
81+
*
82+
*/
83+
class FunctionObjectChainTest extends PHPUnit_Smarty
84+
{
85+
public function setUp(): void
86+
{
87+
$this->setUpSmarty(__DIR__);
88+
89+
// Register modifier plugins that return chainable objects
90+
// These are called like functions: {collect($data)}
91+
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'collect', 'smarty_modifier_collect');
92+
$this->smarty->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'create_object', 'smarty_modifier_create_object');
93+
}
94+
95+
public function testInit()
96+
{
97+
$this->cleanDirs();
98+
}
99+
100+
/**
101+
* Test the NEW feature: function call followed by method chain
102+
* This is the core test for: {$x = collect($data)->filter()->values()->toJson()}
103+
*/
104+
public function testFunctionCallWithMethodChain()
105+
{
106+
$data = [1, 2, 0, 3, null, 4, '', 5];
107+
$this->smarty->assign('data', $data);
108+
109+
// Test the new syntax: function()->method()->method()->method()
110+
$result = $this->smarty->fetch('string:{collect($data)->filter()->values()->toJson()}');
111+
$this->assertEquals('[1,2,3,4,5]', $result);
112+
}
113+
114+
/**
115+
* Test function call with method chain assigned to a variable
116+
* This tests: {$x = collect($data)->filter()->values()->toJson()}
117+
*/
118+
public function testFunctionCallWithMethodChainAssignment()
119+
{
120+
$data = ['a', 'b', '', 'c', null, 'd'];
121+
$this->smarty->assign('data', $data);
122+
123+
// Test assignment with chained methods
124+
$result = $this->smarty->fetch('string:{$result = collect($data)->filter()->values()->toJson()}{$result}');
125+
$this->assertEquals('["a","b","c","d"]', $result);
126+
}
127+
128+
/**
129+
* Test function call with single method chain
130+
*/
131+
public function testFunctionCallWithSingleMethod()
132+
{
133+
$data = [1, 2, 3];
134+
$this->smarty->assign('data', $data);
135+
136+
$result = $this->smarty->fetch('string:{collect($data)->count()}');
137+
$this->assertEquals('3', $result);
138+
}
139+
140+
/**
141+
* Test function call with nested method chains
142+
*/
143+
public function testFunctionCallWithNestedChain()
144+
{
145+
$result = $this->smarty->fetch('string:{create_object("")->getNext()->getValue()}');
146+
$this->assertEquals('ChainedValue', $result);
147+
}
148+
149+
/**
150+
* Test that old syntax still works (two-step process)
151+
*/
152+
public function testOldSyntaxStillWorks()
153+
{
154+
$data = [1, 2, 0, 3];
155+
$this->smarty->assign('data', $data);
156+
157+
// Old syntax: assign to variable first, then chain
158+
$result = $this->smarty->fetch('string:{$x = collect($data)}{$x->filter()->values()->toJson()}');
159+
$this->assertEquals('[1,2,3]', $result);
160+
}
161+
162+
/**
163+
* Test object chain functionality using template file
164+
*/
165+
public function testFunctionObjectChainFromTemplateFile()
166+
{
167+
$data = ['foo', '', 'bar', null, 'baz'];
168+
$this->smarty->assign('data', $data);
169+
170+
$result = $this->smarty->fetch('test_function_chain.tpl');
171+
// Expected: JSON of filtered array and count
172+
$this->assertStringContainsString('["foo","bar","baz"]', $result);
173+
$this->assertStringContainsString('3', $result);
174+
}
175+
176+
/**
177+
* Test complex chaining with multiple operations
178+
* This demonstrates the power of the new feature
179+
*/
180+
public function testComplexChaining()
181+
{
182+
$data = [1, 2, 0, 3, '', 4, null, 5, false, 6];
183+
$this->smarty->assign('data', $data);
184+
185+
// Complex chain: collect -> filter -> values -> toArray
186+
$result = $this->smarty->fetch('string:{$filtered = collect($data)->filter()->values()->toArray()}{$filtered|@json_encode}');
187+
$this->assertEquals('[1,2,3,4,5,6]', $result);
188+
}
189+
190+
/**
191+
* Test that demonstrates the benefit: one line vs multiple lines
192+
*/
193+
public function testNewSyntaxVsOldSyntax()
194+
{
195+
$data = [10, 20, 0, 30];
196+
$this->smarty->assign('data', $data);
197+
198+
// NEW syntax (single line)
199+
$new = $this->smarty->fetch('string:{collect($data)->filter()->count()}');
200+
201+
// OLD syntax (multiple steps)
202+
$old = $this->smarty->fetch('string:{$temp = collect($data)}{$temp = $temp->filter()}{$temp->count()}');
203+
204+
// Both should produce the same result
205+
$this->assertEquals('3', $new);
206+
$this->assertEquals($new, $old);
207+
}
208+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{* Test template for NEW function object chain feature *}
2+
{* Test: function()->method()->method()->method() *}
3+
{collect($data)->filter()->values()->toJson()}
4+
{collect($data)->filter()->count()}

0 commit comments

Comments
 (0)