Skip to content

Commit 3465b94

Browse files
MAGETWO-70866: Enabling the use of looping (for in ..) into Template.php #9401
2 parents 8c43d81 + c5fdd32 commit 3465b94

File tree

3 files changed

+428
-3
lines changed

3 files changed

+428
-3
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\Framework\Filter;
7+
8+
class TemplateTest extends \PHPUnit\Framework\TestCase
9+
{
10+
/**
11+
* @var Template
12+
*/
13+
private $templateFilter;
14+
15+
protected function setUp()
16+
{
17+
$this->templateFilter = \Magento\TestFramework\ObjectManager::getInstance()->create(Template::class);
18+
}
19+
20+
/**
21+
* @param array $results
22+
* @param array $values
23+
* @dataProvider getFilterForDataProvider
24+
*/
25+
public function testFilterFor($results, $values)
26+
{
27+
$this->templateFilter->setVariables(['order' => $this->getOrder(), 'things' => $this->getThings()]);
28+
$this->assertEquals($results, $this->invokeMethod($this->templateFilter, 'filterFor', [$values]));
29+
}
30+
31+
/**
32+
* @return \Magento\Framework\DataObject
33+
*/
34+
private function getOrder()
35+
{
36+
$order = new \Magento\Framework\DataObject();
37+
$visibleItems = [
38+
[
39+
'sku' => 'ABC123',
40+
'name' => 'Product ABC',
41+
'price' => '123',
42+
'ordered_qty' => '2'
43+
]
44+
];
45+
$order->setAllVisibleItems($visibleItems);
46+
return $order;
47+
}
48+
49+
public function getThings()
50+
{
51+
return [
52+
['name' => 'Richard', 'age' => 24],
53+
['name' => 'Jane', 'age' => 12],
54+
['name' => 'Spot', 'age' => 7],
55+
];
56+
}
57+
58+
/**
59+
* @return array
60+
*/
61+
public function getFilterForDataProvider()
62+
{
63+
$template = <<<TEMPLATE
64+
<ul>
65+
{{for thing in things}}
66+
<li>
67+
{{var loop.index}} name: {{var thing.name}}, lastname: {{var thing.lastname}}, age: {{var thing.age}}
68+
</li>
69+
{{/for}}
70+
</ul>
71+
TEMPLATE;
72+
73+
$expectedResult = <<<EXPECTED_RESULT
74+
<ul>
75+
76+
<li>
77+
0 name: Richard, lastname: , age: 24
78+
</li>
79+
80+
<li>
81+
1 name: Jane, lastname: , age: 12
82+
</li>
83+
84+
<li>
85+
2 name: Spot, lastname: , age: 7
86+
</li>
87+
88+
</ul>
89+
EXPECTED_RESULT;
90+
91+
$template2 = <<<TEMPLATE
92+
<ul>
93+
{{for item in order.all_visible_items}}
94+
<li>
95+
index: {{var loop.index}} sku: {{var item.sku}}
96+
name: {{var item.name}} price: {{var item.price}} quantity: {{var item.ordered_qty}}
97+
</li>
98+
{{/for}}
99+
</ul>
100+
TEMPLATE;
101+
102+
$expectedResult2 = <<<EXPECTED_RESULT
103+
<ul>
104+
105+
<li>
106+
index: 0 sku: ABC123
107+
name: Product ABC price: 123 quantity: 2
108+
</li>
109+
110+
</ul>
111+
EXPECTED_RESULT;
112+
return [
113+
[
114+
$expectedResult,
115+
$template
116+
],
117+
[
118+
$expectedResult2,
119+
$template2
120+
]
121+
];
122+
}
123+
124+
/**
125+
* Call protected/private method of a class.
126+
*
127+
* @param object &$object
128+
* @param string $methodName
129+
* @param array $parameters
130+
*
131+
* @return mixed Method return.
132+
*/
133+
private function invokeMethod(&$object, $methodName, array $parameters = [])
134+
{
135+
$reflection = new \ReflectionClass(get_class($object));
136+
$method = $reflection->getMethod($methodName);
137+
$method->setAccessible(true);
138+
return $method->invokeArgs($object, $parameters);
139+
}
140+
}

lib/internal/Magento/Framework/Filter/Template.php

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,25 @@ class Template implements \Zend_Filter_Interface
1919
*/
2020
const CONSTRUCTION_PATTERN = '/{{([a-z]{0,10})(.*?)}}/si';
2121

22-
/**#@+
23-
* Construction logic regular expression
22+
/**
23+
* Construction `depend` regular expression
2424
*/
2525
const CONSTRUCTION_DEPEND_PATTERN = '/{{depend\s*(.*?)}}(.*?){{\\/depend\s*}}/si';
2626

27+
/**
28+
* Construction `if` regular expression
29+
*/
2730
const CONSTRUCTION_IF_PATTERN = '/{{if\s*(.*?)}}(.*?)({{else}}(.*?))?{{\\/if\s*}}/si';
2831

32+
/**
33+
* Construction `template` regular expression
34+
*/
2935
const CONSTRUCTION_TEMPLATE_PATTERN = '/{{(template)(.*?)}}/si';
3036

31-
/**#@-*/
37+
/**
38+
* Construction `for` regular expression
39+
*/
40+
const LOOP_PATTERN = '/{{for(?P<loopItem>.*? )(in)(?P<loopData>.*?)}}(?P<loopBody>.*?){{\/for}}/si';
3241

3342
/**#@-*/
3443
private $afterFilterCallbacks = [];
@@ -130,6 +139,8 @@ public function filter($value)
130139
}
131140
}
132141

142+
$value = $this->filterFor($value);
143+
133144
if (preg_match_all(self::CONSTRUCTION_PATTERN, $value, $constructions, PREG_SET_ORDER)) {
134145
foreach ($constructions as $construction) {
135146
$callback = [$this, $construction[1] . 'Directive'];
@@ -149,6 +160,56 @@ public function filter($value)
149160
return $value;
150161
}
151162

163+
/**
164+
* Filter the string as template.
165+
*
166+
* @param string $value
167+
* @example syntax {{for item in order.items}} name: {{var item.name}} {{/for}} order items collection.
168+
* @example syntax {{for thing in things}} {{var thing.whatever}} {{/for}} e.g.:custom collection.
169+
* @return string
170+
*/
171+
private function filterFor($value)
172+
{
173+
if (preg_match_all(self::LOOP_PATTERN, $value, $constructions, PREG_SET_ORDER)) {
174+
foreach ($constructions as $construction) {
175+
if (!$this->isValidLoop($construction)) {
176+
return $value;
177+
}
178+
179+
$fullTextToReplace = $construction[0];
180+
$loopData = $this->getVariable($construction['loopData'], '');
181+
182+
$loopTextToReplace = $construction['loopBody'];
183+
$loopItemVariableName = preg_replace('/\s+/', '', $construction['loopItem']);
184+
185+
if (is_array($loopData) || $loopData instanceof \Traversable) {
186+
$replaceText = $this->getLoopReplacementText($loopData, $loopItemVariableName, $loopTextToReplace);
187+
$value = str_replace($fullTextToReplace, $replaceText, $value);
188+
}
189+
}
190+
}
191+
192+
return $value;
193+
}
194+
195+
/**
196+
* Check if the matched construction is valid.
197+
*
198+
* @param array $construction
199+
* @return bool
200+
*/
201+
private function isValidLoop(array $construction)
202+
{
203+
$requiredFields = ['loopBody', 'loopItem', 'loopData'];
204+
$validFields = array_filter(
205+
$requiredFields,
206+
function ($field) use ($construction) {
207+
return isset($construction[$field]) && strlen(trim($construction[$field]));
208+
}
209+
);
210+
return count($requiredFields) == count($validFields);
211+
}
212+
152213
/**
153214
* Runs callbacks that have been added to filter content after directive processing is finished.
154215
*
@@ -370,4 +431,53 @@ protected function getStackArgs($stack)
370431
}
371432
return $stack;
372433
}
434+
435+
/**
436+
* Process loop text to replace.
437+
*
438+
* @param array $loopData
439+
* @param string $loopItemVariableName
440+
* @param string $loopTextToReplace
441+
* @return string
442+
*/
443+
private function getLoopReplacementText(array $loopData, $loopItemVariableName, $loopTextToReplace)
444+
{
445+
$loopText = [];
446+
$loopIndex = 0;
447+
$loopDataObject = new \Magento\Framework\DataObject();
448+
449+
foreach ($loopData as $loopItemDataObject) {
450+
// Loop item can be an array or DataObject.
451+
// If loop item is an array, convert it to DataObject
452+
// to have unified interface if the collection
453+
if (!$loopItemDataObject instanceof \Magento\Framework\DataObject) {
454+
if (!is_array($loopItemDataObject)) {
455+
continue;
456+
}
457+
$loopItemDataObject = new \Magento\Framework\DataObject($loopItemDataObject);
458+
}
459+
460+
$loopDataObject->setData('index', $loopIndex++);
461+
$this->templateVars['loop'] = $loopDataObject;
462+
$this->templateVars[$loopItemVariableName] = $loopItemDataObject;
463+
464+
if (preg_match_all(
465+
self::CONSTRUCTION_PATTERN,
466+
$loopTextToReplace,
467+
$attributes,
468+
PREG_SET_ORDER
469+
)
470+
) {
471+
$subText = $loopTextToReplace;
472+
foreach ($attributes as $attribute) {
473+
$text = $this->getVariable($attribute[2], '');
474+
$subText = str_replace($attribute[0], $text, $subText);
475+
}
476+
$loopText[] = $subText;
477+
}
478+
unset($this->templateVars[$loopItemVariableName]);
479+
}
480+
$replaceText = implode('', $loopText);
481+
return $replaceText;
482+
}
373483
}

0 commit comments

Comments
 (0)