Browse code

Optimize usage of Traversable/Iterator

SpacePossum authored on 18/11/2016 10:38:22 • Fabien Potencier committed on 27/12/2016 10:56:06
Showing 2 changed files
... ...
@@ -652,7 +652,7 @@ function twig_array_merge($arr1, $arr2)
652 652
 function twig_slice(Twig_Environment $env, $item, $start, $length = null, $preserveKeys = false)
653 653
 {
654 654
     if ($item instanceof Traversable) {
655
-        if ($item instanceof IteratorAggregate) {
655
+        while ($item instanceof IteratorAggregate) {
656 656
             $item = $item->getIterator();
657 657
         }
658 658
 
... ...
@@ -821,7 +821,27 @@ function _twig_default_filter($value, $default = '')
821 821
 function twig_get_array_keys_filter($array)
822 822
 {
823 823
     if ($array instanceof Traversable) {
824
-        return array_keys(iterator_to_array($array));
824
+        while ($array instanceof IteratorAggregate) {
825
+            $array = $array->getIterator();
826
+        }
827
+
828
+        if ($array instanceof Iterator) {
829
+            $keys = array();
830
+            $array->rewind();
831
+            while ($array->valid()) {
832
+                $keys[] = $array->key();
833
+                $array->next();
834
+            }
835
+
836
+            return $keys;
837
+        }
838
+
839
+        $keys = array();
840
+        foreach ($array as $key => $item) {
841
+            $keys[] = $key;
842
+        }
843
+
844
+        return $keys;
825 845
     }
826 846
 
827 847
     if (!is_array($array)) {
... ...
@@ -901,7 +921,21 @@ function twig_in_filter($value, $compare)
901 921
     } elseif (is_string($compare) && (is_string($value) || is_int($value) || is_float($value))) {
902 922
         return '' === $value || false !== strpos($compare, (string) $value);
903 923
     } elseif ($compare instanceof Traversable) {
904
-        return in_array($value, iterator_to_array($compare, false), is_object($value) || is_resource($value));
924
+        if (is_object($value) || is_resource($value)) {
925
+            foreach ($compare as $item) {
926
+                if ($item === $value) {
927
+                    return true;
928
+                }
929
+            }
930
+        } else {
931
+            foreach ($compare as $item) {
932
+                if ($item == $value) {
933
+                    return true;
934
+                }
935
+            }
936
+        }
937
+
938
+        return false;
905 939
     }
906 940
 
907 941
     return false;
... ...
@@ -115,14 +115,24 @@ class Twig_Tests_Extension_CoreTest extends PHPUnit_Framework_TestCase
115 115
         $this->assertEquals($output, 'éÄ');
116 116
     }
117 117
 
118
-    public function testCustomEscaper()
118
+    /**
119
+     * @dataProvider provideCustomEscaperCases
120
+     */
121
+    public function testCustomEscaper($expected, $string, $strategy)
119 122
     {
120 123
         $twig = new Twig_Environment($this->getMockBuilder('Twig_LoaderInterface')->getMock());
121 124
         $twig->getExtension('Twig_Extension_Core')->setEscaper('foo', 'foo_escaper_for_test');
122 125
 
123
-        $this->assertEquals('fooUTF-8', twig_escape_filter($twig, 'foo', 'foo'));
124
-        $this->assertEquals('UTF-8', twig_escape_filter($twig, null, 'foo'));
125
-        $this->assertEquals('42UTF-8', twig_escape_filter($twig, 42, 'foo'));
126
+        $this->assertSame($expected, twig_escape_filter($twig, $string, $strategy));
127
+    }
128
+
129
+    public function provideCustomEscaperCases()
130
+    {
131
+        return array(
132
+            array('fooUTF-8', 'foo', 'foo'),
133
+            array('UTF-8', null, 'foo'),
134
+            array('42UTF-8', 42, 'foo'),
135
+        );
126 136
     }
127 137
 
128 138
     /**
... ...
@@ -133,22 +143,129 @@ class Twig_Tests_Extension_CoreTest extends PHPUnit_Framework_TestCase
133 143
         twig_escape_filter(new Twig_Environment($this->getMockBuilder('Twig_LoaderInterface')->getMock()), 'foo', 'bar');
134 144
     }
135 145
 
136
-    public function testTwigFirst()
146
+    /**
147
+     * @dataProvider provideTwigFirstCases
148
+     */
149
+    public function testTwigFirst($expected, $input)
137 150
     {
138 151
         $twig = new Twig_Environment($this->getMockBuilder('Twig_LoaderInterface')->getMock());
139
-        $this->assertEquals('a', twig_first($twig, 'abc'));
140
-        $this->assertEquals(1, twig_first($twig, array(1, 2, 3)));
141
-        $this->assertSame('', twig_first($twig, null));
142
-        $this->assertSame('', twig_first($twig, ''));
152
+        $this->assertSame($expected, twig_first($twig, $input));
143 153
     }
144 154
 
145
-    public function testTwigLast()
155
+    public function provideTwigFirstCases()
156
+    {
157
+        $i = array(1 => 'a', 2 => 'b', 3 => 'c');
158
+
159
+        return array(
160
+            array('a', 'abc'),
161
+            array(1, array(1, 2, 3)),
162
+            array('', null),
163
+            array('', ''),
164
+            array('a', new CoreTestIterator($i, array_keys($i), true, 3)),
165
+        );
166
+    }
167
+
168
+    /**
169
+     * @dataProvider provideTwigLastCases
170
+     */
171
+    public function testTwigLast($expected, $input)
146 172
     {
147 173
         $twig = new Twig_Environment($this->getMockBuilder('Twig_LoaderInterface')->getMock());
148
-        $this->assertEquals('c', twig_last($twig, 'abc'));
149
-        $this->assertEquals(3, twig_last($twig, array(1, 2, 3)));
150
-        $this->assertSame('', twig_last($twig, null));
151
-        $this->assertSame('', twig_last($twig, ''));
174
+        $this->assertSame($expected, twig_last($twig, $input));
175
+    }
176
+
177
+    public function provideTwigLastCases()
178
+    {
179
+        $i = array(1 => 'a', 2 => 'b', 3 => 'c');
180
+
181
+        return array(
182
+            array('c', 'abc'),
183
+            array(3, array(1, 2, 3)),
184
+            array('', null),
185
+            array('', ''),
186
+            array('c', new CoreTestIterator($i, array_keys($i), true)),
187
+        );
188
+    }
189
+
190
+    /**
191
+     * @dataProvider provideArrayKeyCases
192
+     */
193
+    public function testArrayKeysFilter(array $expected, $input)
194
+    {
195
+        $this->assertSame($expected, twig_get_array_keys_filter($input));
196
+    }
197
+
198
+    public function provideArrayKeyCases()
199
+    {
200
+        $array = array('a' => 'a1', 'b' => 'b1', 'c' => 'c1');
201
+        $keys = array_keys($array);
202
+
203
+        return array(
204
+            array($keys, $array),
205
+            array($keys, new CoreTestIterator($array, $keys)),
206
+            array($keys, new CoreTestIteratorAggregate($array, $keys)),
207
+            array($keys, new CoreTestIteratorAggregateAggregate($array, $keys)),
208
+            array(array(), null),
209
+            array(array('a'), new SimpleXMLElement('<xml><a></a></xml>')),
210
+        );
211
+    }
212
+
213
+    /**
214
+     * @dataProvider provideInFilterCases
215
+     */
216
+    public function testInFilter($expected, $value, $compare)
217
+    {
218
+        $this->assertSame($expected, twig_in_filter($value, $compare));
219
+    }
220
+
221
+    public function provideInFilterCases()
222
+    {
223
+        $array = array(1, 2, 'a' => 3, 5, 6, 7);
224
+        $keys = array_keys($array);
225
+
226
+        return array(
227
+            array(true, 1, $array),
228
+            array(true, '3', $array),
229
+            array(true, '3', 'abc3def'),
230
+            array(true, 1, new CoreTestIterator($array, $keys, true, 1)),
231
+            array(true, '3', new CoreTestIterator($array, $keys, true, 3)),
232
+            array(true, '3', new CoreTestIteratorAggregateAggregate($array, $keys, true, 3)),
233
+            array(false, 4, $array),
234
+            array(false, 4, new CoreTestIterator($array, $keys, true)),
235
+            array(false, 4, new CoreTestIteratorAggregateAggregate($array, $keys, true)),
236
+            array(false, 1, 1),
237
+            array(true, 'b', new SimpleXMLElement('<xml><a>b</a></xml>')),
238
+        );
239
+    }
240
+
241
+    /**
242
+     * @dataProvider provideSliceFilterCases
243
+     */
244
+    public function testSliceFilter($expected, $input, $start, $length = null, $preserveKeys = false)
245
+    {
246
+        $twig = new Twig_Environment($this->getMockBuilder('Twig_LoaderInterface')->getMock());
247
+        $this->assertSame($expected, twig_slice($twig, $input, $start, $length, $preserveKeys));
248
+    }
249
+
250
+    public function provideSliceFilterCases()
251
+    {
252
+        $i = array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4);
253
+        $keys = array_keys($i);
254
+
255
+        return array(
256
+            array(array('a' => 1), $i, 0, 1, true),
257
+            array(array('a' => 1), $i, 0, 1, false),
258
+            array(array('b' => 2, 'c' => 3), $i, 1, 2),
259
+            array(array(1), array(1, 2, 3, 4), 0, 1),
260
+            array(array(2, 3), array(1, 2, 3, 4), 1, 2),
261
+            array(array(2, 3), new CoreTestIterator($i, $keys, true), 1, 2),
262
+            array(array('c' => 3, 'd' => 4), new CoreTestIteratorAggregate($i, $keys, true), 2, null, true),
263
+            array($i, new CoreTestIterator($i, $keys, true), 0, count($keys) + 10, true),
264
+            array(array(), new CoreTestIterator($i, $keys, true), count($keys) + 10),
265
+            array('de', 'abcdef', 3, 2),
266
+            array(array(), new SimpleXMLElement('<items><item>1</item><item>2</item></items>'), 3),
267
+            array(array(), new ArrayIterator(array(1, 2)), 3)
268
+        );
152 269
     }
153 270
 }
154 271
 
... ...
@@ -156,3 +273,83 @@ function foo_escaper_for_test(Twig_Environment $env, $string, $charset)
156 273
 {
157 274
     return $string.$charset;
158 275
 }
276
+
277
+final class CoreTestIteratorAggregate implements IteratorAggregate
278
+{
279
+    private $iterator;
280
+
281
+    public function __construct(array $array, array $keys, $allowAccess = false, $maxPosition = false)
282
+    {
283
+        $this->iterator = new CoreTestIterator($array, $keys, $allowAccess, $maxPosition);
284
+    }
285
+
286
+    public function getIterator()
287
+    {
288
+        return $this->iterator;
289
+    }
290
+}
291
+
292
+final class CoreTestIteratorAggregateAggregate implements IteratorAggregate
293
+{
294
+    private $iterator;
295
+
296
+    public function __construct(array $array, array $keys, $allowValueAccess = false, $maxPosition = false)
297
+    {
298
+        $this->iterator = new CoreTestIteratorAggregate($array, $keys, $allowValueAccess, $maxPosition);
299
+    }
300
+
301
+    public function getIterator()
302
+    {
303
+        return $this->iterator;
304
+    }
305
+}
306
+
307
+final class CoreTestIterator implements Iterator
308
+{
309
+    private $position;
310
+    private $array;
311
+    private $arrayKeys;
312
+    private $allowValueAccess;
313
+    private $maxPosition;
314
+
315
+    public function __construct(array $values, array $keys, $allowValueAccess = false, $maxPosition = false)
316
+    {
317
+        $this->array = $values;
318
+        $this->arrayKeys = $keys;
319
+        $this->position = 0;
320
+        $this->allowValueAccess = $allowValueAccess;
321
+        $this->maxPosition = false === $maxPosition ? count($values) + 1 : $maxPosition;
322
+    }
323
+
324
+    public function rewind()
325
+    {
326
+        $this->position = 0;
327
+    }
328
+
329
+    public function current()
330
+    {
331
+        if ($this->allowValueAccess) {
332
+            return $this->array[$this->key()];
333
+        }
334
+
335
+        throw new \LogicException('Code should only use the keys, not the values provided by iterator.');
336
+    }
337
+
338
+    public function key()
339
+    {
340
+        return $this->arrayKeys[$this->position];
341
+    }
342
+
343
+    public function next()
344
+    {
345
+        ++$this->position;
346
+        if ($this->position === $this->maxPosition) {
347
+             throw new \LogicException(sprintf('Code should not iterate beyond %d.', $this->maxPosition));
348
+        }
349
+    }
350
+
351
+    public function valid()
352
+    {
353
+        return isset($this->arrayKeys[$this->position]);
354
+    }
355
+}